diff options
-rw-r--r-- | src/com/android/providers/contacts/ContactsProvider2.java | 117 | ||||
-rw-r--r-- | tests/src/com/android/providers/contacts/ContactsProvider2Test.java | 78 |
2 files changed, 125 insertions, 70 deletions
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java index 9497d24..9e39b94 100644 --- a/src/com/android/providers/contacts/ContactsProvider2.java +++ b/src/com/android/providers/contacts/ContactsProvider2.java @@ -110,6 +110,7 @@ import android.provider.ContactsContract.CommonDataKinds.Note; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.SipAddress; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.ContactCounts; @@ -664,6 +665,36 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun .add(TIMES_USED_SORT_COLUMN, "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")") .build(); + /** + * Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows + * users to obtain part of Data columns. Right now Starred part just returns NULL for + * those data columns (frequent part should return real ones in data table). + **/ + private static final ProjectionMap sStrequentPhoneOnlyStarredProjectionMap + = ProjectionMap.builder() + .addAll(sContactsProjectionMap) + .add(TIMES_USED_SORT_COLUMN, String.valueOf(Long.MAX_VALUE)) + .add(Phone.NUMBER, "NULL") + .add(Phone.TYPE, "NULL") + .add(Phone.LABEL, "NULL") + .build(); + + /** + * Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows + * users to obtain part of Data columns. We hard-code {@link Contacts#IS_USER_PROFILE} to NULL, + * because sContactsProjectionMap specifies a field that doesn't exist in the view behind the + * query that uses this projection map. + **/ + private static final ProjectionMap sStrequentPhoneOnlyFrequentProjectionMap + = ProjectionMap.builder() + .addAll(sContactsProjectionMap) + .add(TIMES_USED_SORT_COLUMN, DataUsageStatColumns.CONCRETE_TIMES_USED) + .add(Phone.NUMBER) + .add(Phone.TYPE) + .add(Phone.LABEL) + .add(Contacts.IS_USER_PROFILE, "NULL") + .build(); + /** Contains just the contacts vCard columns */ private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder() .add(Contacts._ID) @@ -4487,9 +4518,10 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun } // Build the first query for starred - setTablesAndProjectionMapForContacts(qb, uri, projection, - false /* for frequent */, phoneOnly); - qb.setProjectionMap(sStrequentStarredProjectionMap); + setTablesAndProjectionMapForContacts(qb, uri, projection, false); + qb.setProjectionMap(phoneOnly ? + sStrequentPhoneOnlyStarredProjectionMap + : sStrequentStarredProjectionMap); qb.appendWhere(DbQueryUtils.concatenateClauses( selection, Contacts.IS_USER_PROFILE + "=0")); qb.setStrict(true); @@ -4498,17 +4530,51 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun // Reset the builder. qb = new SQLiteQueryBuilder(); - - // Build the second query for frequent - setTablesAndProjectionMapForContacts(qb, uri, projection, - true /* for frequent */, phoneOnly); - qb.setProjectionMap(sStrequentFrequentProjectionMap); - qb.appendWhere(DbQueryUtils.concatenateClauses( - selection, Contacts.IS_USER_PROFILE + "=0")); qb.setStrict(true); - final String frequentQuery = qb.buildQuery(subProjection, - "(" + Contacts.STARRED + " =0 OR " + Contacts.STARRED + " IS NULL)", - Contacts._ID, null, null, null); + + // Build the second query for frequent part. + final String frequentQuery; + if (phoneOnly) { + final StringBuilder tableBuilder = new StringBuilder(); + // In phone only mode, we need to look at view_data instead of + // contacts/raw_contacts to obtain actual phone numbers. One problem is that + // view_data is much larger than view_contacts, so our query might become much + // slower. + // + // To avoid the possible slow down, we start from data usage table and join + // view_data to the table, assuming data usage table is quite smaller than + // 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 + + " INNER JOIN " + Views.DATA + " " + Tables.DATA + + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" + + DataColumns.CONCRETE_ID + " AND " + + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + + DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")"); + appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID); + appendContactStatusUpdateJoin(tableBuilder, projection, + ContactsColumns.LAST_STATUS_UPDATE_ID); + + qb.setTables(tableBuilder.toString()); + qb.setProjectionMap(sStrequentPhoneOnlyFrequentProjectionMap); + qb.appendWhere(DbQueryUtils.concatenateClauses( + selection, + Contacts.STARRED + "=0 OR " + Contacts.STARRED + " IS NULL", + MimetypesColumns.MIMETYPE + " IN (" + + "'" + Phone.CONTENT_ITEM_TYPE + "', " + + "'" + SipAddress.CONTENT_ITEM_TYPE + "')")); + frequentQuery = qb.buildQuery(subProjection, null, null, null, null, null); + } else { + setTablesAndProjectionMapForContacts(qb, uri, projection, true); + qb.setProjectionMap(sStrequentFrequentProjectionMap); + qb.appendWhere(DbQueryUtils.concatenateClauses( + selection, + "(" + Contacts.STARRED + " =0 OR " + Contacts.STARRED + " IS NULL)", + Contacts.IS_USER_PROFILE + "=0")); + frequentQuery = qb.buildQuery(subProjection, + null, Contacts._ID, null, null, null); + } // Put them together final String unionQuery = @@ -5619,38 +5685,27 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, String[] projection) { - setTablesAndProjectionMapForContacts(qb, uri, projection, false, false); + setTablesAndProjectionMapForContacts(qb, uri, projection, false); } /** - * @param forStrequentFrequent Should be used only in strequent handling. - * true when this is for frequently contacted listing (not starred) - * @param strequentPhoneCallOnly Should be used only in strequent handling. - * true when this is for phone-only results. See also - * {@link ContactsContract#STREQUENT_PHONE_ONLY}. + * @param includeDataUsageStat true when the table should include DataUsageStat table. + * Note that this uses INNER JOIN instead of LEFT OUTER JOIN, so some of data in Contacts + * may be dropped. */ private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri, - String[] projection, boolean forStrequentFrequent, boolean strequentPhoneCallOnly) { + String[] projection, boolean includeDataUsageStat) { StringBuilder sb = new StringBuilder(); sb.append(Views.CONTACTS); // Just for frequently contacted contacts in Strequent Uri handling. - if (forStrequentFrequent) { - final String strequentPhoneCallOnlyClause = - (strequentPhoneCallOnly ? DbQueryUtils.concatenateClauses( - MimetypesColumns.MIMETYPE + "=\'" + Phone.CONTENT_ITEM_TYPE + "'", - DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + - DataUsageStatColumns.USAGE_TYPE_INT_CALL) - : ""); - // Use INNER JOIN for maximum performance, ommiting unnecessary rows as much as - // possible. + if (includeDataUsageStat) { sb.append(" INNER JOIN " + Views.DATA_USAGE_STAT + " AS " + Tables.DATA_USAGE_STAT + " ON (" + DbQueryUtils.concatenateClauses( DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0", - RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID, - strequentPhoneCallOnlyClause) + + RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) + ")"); } diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java index df83373..44250f7 100644 --- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java +++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java @@ -1202,10 +1202,13 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { /** Tests {@link DataUsageFeedback} correctly promotes a data row instead of a raw contact. */ public void testEmailFilterSortOrderWithFeedback() { long rawContactId1 = createRawContact(); - insertEmail(rawContactId1, "address1@email.com"); + String address1 = "address1@email.com"; + insertEmail(rawContactId1, address1); long rawContactId2 = createRawContact(); - insertEmail(rawContactId2, "address2@email.com"); - long dataId = ContentUris.parseId(insertEmail(rawContactId2, "address3@email.com")); + String address2 = "address2@email.com"; + insertEmail(rawContactId2, address2); + String address3 = "address3@email.com"; + ContentUris.parseId(insertEmail(rawContactId2, address3)); ContentValues v1 = new ContentValues(); v1.put(Email.ADDRESS, "address1@email.com"); @@ -1232,13 +1235,7 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { assertStoredValuesOrderly(filterUri3, new ContentValues[] { v1, v2, v3 }); assertStoredValuesOrderly(filterUri4, new ContentValues[] { v1, v2, v3 }); - // Send feedback for address3 in the second account. - Uri feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() - .appendPath(String.valueOf(dataId)) - .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, - DataUsageFeedback.USAGE_TYPE_LONG_TEXT) - .build(); - assertNotSame(0, mResolver.update(feedbackUri, new ContentValues(), null, null)); + sendFeedback(address3, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, v3); // account3@email.com should be the first. account2@email.com should also be promoted as // it has same contact id. @@ -1454,8 +1451,9 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { createContact(values1, "Noah", "Tever", "18004664411", email1, StatusUpdates.OFFLINE, timesContacted1, 0, 0, StatusUpdates.CAPABILITY_HAS_CAMERA | StatusUpdates.CAPABILITY_HAS_VIDEO); + final String phoneNumber2 = "18004664412"; ContentValues values2 = new ContentValues(); - createContact(values2, "Sam", "Times", "18004664412", + createContact(values2, "Sam", "Times", phoneNumber2, "b@acme.com", StatusUpdates.INVISIBLE, 3, 0, 0, StatusUpdates.CAPABILITY_HAS_CAMERA); ContentValues values3 = new ContentValues(); @@ -1473,20 +1471,8 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { // usage feedback should be used for "frequently contacted" listing. assertStoredValues(Contacts.CONTENT_STREQUENT_URI, values4); - final long dataIdPhone3 = getStoredLongValue(Phone.CONTENT_URI, - Phone.NUMBER + "=?", new String[] { phoneNumber3 }, - Data._ID); - // Send feedback for the 3rd phone number, pretending we called that person via phone. - Uri feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() - .appendPath(String.valueOf(dataIdPhone3)) - .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, - DataUsageFeedback.USAGE_TYPE_CALL) - .build(); - assertNotSame(0, mResolver.update(feedbackUri, new ContentValues(), null, null)); - - // After the feedback, times contacted should be incremented - values3.put(RawContacts.TIMES_CONTACTED, timesContacted3 + 1); + sendFeedback(phoneNumber3, DataUsageFeedback.USAGE_TYPE_CALL, values3); // After the feedback, 3rd contact should be shown after starred one. assertStoredValuesOrderly(Contacts.CONTENT_STREQUENT_URI, @@ -1497,31 +1483,26 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { Email.ADDRESS + "=?", new String[] { email1}, Email._ID); - // Send feedback for the 1st email, pretending we sent the person an email twice. - // (we don't define the order for 1st and 3rd contacts with same times contacted) - feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() - .appendPath(String.valueOf(dataIdEmail1)) - .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, - DataUsageFeedback.USAGE_TYPE_LONG_TEXT) - .build(); - assertNotSame(0, mResolver.update(feedbackUri, new ContentValues(), null, null)); + sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1); // Twice. - assertNotSame(0, mResolver.update(feedbackUri, new ContentValues(), null, null)); - - // After the feedback, times contacted should be incremented - values1.put(RawContacts.TIMES_CONTACTED, timesContacted1 + 2); + sendFeedback(email1, DataUsageFeedback.USAGE_TYPE_LONG_TEXT, values1); // After the feedback, 1st and 3rd contacts should be shown after starred one. assertStoredValuesOrderly(Contacts.CONTENT_STREQUENT_URI, - new ContentValues[] { values4, values1, values3 }); + new ContentValues[] { values4, values1, values3 }); // With phone-only parameter, the 1st contact shouldn't be returned, since it is only // about email, not phone-call. Uri phoneOnlyStrequentUri = Contacts.CONTENT_STREQUENT_URI.buildUpon() .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true") .build(); - assertStoredValuesOrderly(phoneOnlyStrequentUri, - new ContentValues[] { values4, values3 }); + assertStoredValuesOrderly(phoneOnlyStrequentUri, new ContentValues[] { values4, values3 }); + + // Send feedback for the 2rd phone number, pretending we send the person a SMS message. + sendFeedback(phoneNumber2, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT, values1); + + // SMS feedback shouldn't affect phone-only results. + assertStoredValuesOrderly(phoneOnlyStrequentUri, new ContentValues[] { values4, values3 }); Uri filterUri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI, "fay"); assertStoredValues(filterUri, values4); @@ -5672,5 +5653,24 @@ public class ContactsProvider2Test extends BaseContactsProvider2Test { values.put(Data.SYNC3, "sync3"); values.put(Data.SYNC4, "sync4"); } + + /** + * @param data1 email address or phone number + * @param usageType One of {@link DataUsageFeedback#USAGE_TYPE} + * @param values ContentValues for this feedback. Useful for incrementing + * {Contacts#TIMES_CONTACTED} in the ContentValue. Can be null. + */ + private void sendFeedback(String data1, String usageType, ContentValues values) { + final long dataId = getStoredLongValue(Data.CONTENT_URI, + Data.DATA1 + "=?", new String[] { data1 }, Data._ID); + final Uri feedbackUri = DataUsageFeedback.FEEDBACK_URI.buildUpon() + .appendPath(String.valueOf(dataId)) + .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, usageType) + .build(); + assertNotSame(0, mResolver.update(feedbackUri, new ContentValues(), null, null)); + if (values != null && values.containsKey(Contacts.TIMES_CONTACTED)) { + values.put(Contacts.TIMES_CONTACTED, values.getAsInteger(Contacts.TIMES_CONTACTED) + 1); + } + } } |