am c085b3ee: Avoid long running upgrade work on the main thread
diff --git a/Android.mk b/Android.mk
index 5e7b6b9..ade9f46 100644
--- a/Android.mk
+++ b/Android.mk
@@ -10,7 +10,7 @@
LOCAL_JAVA_LIBRARIES := ext
-LOCAL_STATIC_JAVA_LIBRARIES += android-common
+LOCAL_STATIC_JAVA_LIBRARIES += android-common com.android.vcard
LOCAL_PACKAGE_NAME := ContactsProvider
LOCAL_CERTIFICATE := shared
diff --git a/src/com/android/providers/contacts/ContactAggregator.java b/src/com/android/providers/contacts/ContactAggregator.java
index 5f80115..8fbc8de 100644
--- a/src/com/android/providers/contacts/ContactAggregator.java
+++ b/src/com/android/providers/contacts/ContactAggregator.java
@@ -1088,9 +1088,8 @@
+ Contacts.STARRED + ", "
+ Contacts.HAS_PHONE_NUMBER + ", "
+ ContactsColumns.SINGLE_IS_RESTRICTED + ", "
- + Contacts.LOOKUP_KEY + ", "
- + Contacts.IN_VISIBLE_GROUP + ") " +
- " VALUES (?,?,?,?,?,?,?,?,?,?,0)";
+ + Contacts.LOOKUP_KEY + ") " +
+ " VALUES (?,?,?,?,?,?,?,?,?,?)";
int NAME_RAW_CONTACT_ID = 1;
int PHOTO_ID = 2;
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index edd73ff..85067cb 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -74,7 +74,7 @@
/* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "ContactsDatabaseHelper";
- static final int DATABASE_VERSION = 309;
+ static final int DATABASE_VERSION = 401;
private static final String DATABASE_NAME = "contacts2.db";
private static final String DATABASE_PRESENCE = "presence_db";
@@ -99,6 +99,7 @@
public static final String STATUS_UPDATES = "status_updates";
public static final String PROPERTIES = "properties";
public static final String ACCOUNTS = "accounts";
+ public static final String VISIBLE_CONTACTS = "visible_contacts";
public static final String DATA_JOIN_MIMETYPES = "data "
+ "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
@@ -214,6 +215,11 @@
final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND "
+ Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?";
+
+ public static final String CONTACT_VISIBLE =
+ "EXISTS (SELECT _id FROM " + Tables.VISIBLE_CONTACTS
+ + " WHERE " + Tables.CONTACTS +"." + Contacts._ID
+ + "=" + Tables.VISIBLE_CONTACTS +"." + Contacts._ID + ")";
}
public interface ContactsColumns {
@@ -271,7 +277,6 @@
public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
public static final String AGGREGATION_NEEDED = "aggregation_needed";
- public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group";
public static final String CONCRETE_DISPLAY_NAME =
Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
@@ -470,12 +475,6 @@
private final Context mContext;
private final SyncStateContentProviderHelper mSyncState;
-
- /** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */
- private SQLiteStatement mVisibleSpecificUpdate;
- private SQLiteStatement mVisibleUpdateRawContacts;
- private SQLiteStatement mVisibleSpecificUpdateRawContacts;
-
private boolean mReopenDatabase = false;
private static ContactsDatabaseHelper sSingleton = null;
@@ -541,34 +540,6 @@
+ " FROM " + Tables.ACTIVITIES_JOIN_MIMETYPES + " WHERE " + Tables.ACTIVITIES + "."
+ Activities._ID + "=?");
- // Change visibility of a specific contact
- mVisibleSpecificUpdate = db.compileStatement(
- "UPDATE " + Tables.CONTACTS +
- " SET " + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")" +
- " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
-
- // Return visibility of the aggregate contact joined with the raw contact
- String contactVisibility =
- "SELECT " + Contacts.IN_VISIBLE_GROUP +
- " FROM " + Tables.CONTACTS +
- " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID;
-
- // Set visibility of raw contacts to the visibility of corresponding aggregate contacts
- mVisibleUpdateRawContacts = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(CASE WHEN ("
- + contactVisibility + ")=1 THEN 1 ELSE 0 END)" +
- " WHERE " + RawContacts.DELETED + "=0" +
- " AND " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "!=("
- + contactVisibility + ")=1");
-
- // Set visibility of a raw contact to the visibility of corresponding aggregate contact
- mVisibleSpecificUpdateRawContacts = db.compileStatement(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=("
- + contactVisibility + ")" +
- " WHERE " + RawContacts.DELETED + "=0 AND " + RawContacts.CONTACT_ID + "=?");
-
db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
@@ -651,17 +622,12 @@
Contacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
Contacts.LAST_TIME_CONTACTED + " INTEGER," +
Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
- Contacts.IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 1," +
Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
Contacts.LOOKUP_KEY + " TEXT," +
ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)," +
ContactsColumns.SINGLE_IS_RESTRICTED + " INTEGER NOT NULL DEFAULT 0" +
");");
- db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
- Contacts.IN_VISIBLE_GROUP +
- ");");
-
db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" +
Contacts.HAS_PHONE_NUMBER +
");");
@@ -704,7 +670,6 @@
RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE " +
ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
RawContacts.NAME_VERIFIED + " INTEGER NOT NULL DEFAULT 0," +
- RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," +
RawContacts.SYNC1 + " TEXT, " +
RawContacts.SYNC2 + " TEXT, " +
RawContacts.SYNC3 + " TEXT, " +
@@ -851,6 +816,8 @@
Groups.DELETED + " INTEGER NOT NULL DEFAULT 0," +
Groups.GROUP_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1," +
+ Groups.AUTO_ADD + " INTEGER NOT NULL DEFAULT 0," +
+ Groups.FAVORITES + " INTEGER NOT NULL DEFAULT 0," +
Groups.SYNC1 + " TEXT, " +
Groups.SYNC2 + " TEXT, " +
Groups.SYNC3 + " TEXT, " +
@@ -893,6 +860,10 @@
Settings.ACCOUNT_TYPE + ") ON CONFLICT REPLACE" +
");");
+ db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
+ Contacts._ID + " INTEGER PRIMARY KEY" +
+ ");");
+
// The table for recent calls is here so we can do table joins
// on people, phones, and calls all in one place.
db.execSQL("CREATE TABLE " + Tables.CALLS + " (" +
@@ -991,6 +962,11 @@
+ "=OLD." + RawContacts._ID
+ " OR " + AggregationExceptions.RAW_CONTACT_ID2
+ "=OLD." + RawContacts._ID + ";"
+ + " DELETE FROM " + Tables.VISIBLE_CONTACTS
+ + " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
+ + " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
+ + " WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
+ + " )=1;"
+ " DELETE FROM " + Tables.CONTACTS
+ " WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
+ " AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
@@ -1066,13 +1042,11 @@
db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key1_index");
db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
- RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
RawContacts.SORT_KEY_PRIMARY +
");");
db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key2_index");
db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
- RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
RawContacts.SORT_KEY_ALTERNATIVE +
");");
}
@@ -1149,9 +1123,7 @@
+ "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY
+ " AS " + Contacts.SORT_KEY_PRIMARY + ", "
+ "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE
- + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", "
- + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
- + " AS " + Contacts.IN_VISIBLE_GROUP;
+ + " AS " + Contacts.SORT_KEY_ALTERNATIVE;
String dataSelect = "SELECT "
+ DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
@@ -1163,7 +1135,8 @@
+ contactNameColumns + ", "
+ Contacts.LOOKUP_KEY + ", "
+ Contacts.PHOTO_ID + ", "
- + Contacts.NAME_RAW_CONTACT_ID + ","
+ + Contacts.NAME_RAW_CONTACT_ID + ", "
+ + Clauses.CONTACT_VISIBLE + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
+ ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
+ Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
+ " FROM " + Tables.DATA
@@ -1228,7 +1201,10 @@
+ " AS " + Contacts.STARRED + ", "
+ ContactsColumns.CONCRETE_TIMES_CONTACTED
+ " AS " + Contacts.TIMES_CONTACTED + ", "
- + ContactsColumns.LAST_STATUS_UPDATE_ID;
+ + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
+ + Contacts.NAME_RAW_CONTACT_ID + ", "
+ + Clauses.CONTACT_VISIBLE + " AS " + Contacts.IN_VISIBLE_GROUP;
+
String contactsSelect = "SELECT "
+ ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
@@ -1257,6 +1233,8 @@
+ Groups.DELETED + ","
+ Groups.GROUP_VISIBLE + ","
+ Groups.SHOULD_SYNC + ","
+ + Groups.AUTO_ADD + ","
+ + Groups.FAVORITES + ","
+ Groups.SYNC1 + ","
+ Groups.SYNC2 + ","
+ Groups.SYNC3 + ","
@@ -1488,6 +1466,24 @@
oldVersion = 309;
}
+ if (oldVersion == 309) {
+ // Add column NAME_RAW_CONTACT_ID
+ upgradeViewsAndTriggers = true;
+ oldVersion = 310;
+ }
+
+ if (oldVersion == 310) {
+ upgradeViewsAndTriggers = true;
+ upgradeToVersion311(db);
+ oldVersion = 311;
+ }
+
+ if (oldVersion == 311) {
+ upgradeViewsAndTriggers = true;
+ upgradeToVersion401(db);
+ oldVersion = 401;
+ }
+
if (upgradeViewsAndTriggers) {
createContactsViews(db);
createGroupsView(db);
@@ -1562,8 +1558,7 @@
" ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)");
db.execSQL(
"ALTER TABLE " + Tables.RAW_CONTACTS +
- " ADD " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
- + " INTEGER NOT NULL DEFAULT 0");
+ " ADD contact_in_visible_group INTEGER NOT NULL DEFAULT 0");
// For each Contact, find the RawContact that contributed the display name
db.execSQL(
@@ -1605,7 +1600,7 @@
// indexing on (display_name, in_visible_group)
db.execSQL(
"UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
+ " SET contact_in_visible_group=(" +
"SELECT " + Contacts.IN_VISIBLE_GROUP +
" FROM " + Tables.CONTACTS +
" WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" +
@@ -1613,7 +1608,7 @@
);
db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
- RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ "contact_in_visible_group" + "," +
RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
");");
@@ -1657,12 +1652,12 @@
db.execSQL("DROP INDEX raw_contact_sort_key1_index");
db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
- RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ "contact_in_visible_group" + "," +
RawContacts.SORT_KEY_PRIMARY +
");");
db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
- RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
+ "contact_in_visible_group" + "," +
RawContacts.SORT_KEY_ALTERNATIVE +
");");
}
@@ -2103,13 +2098,20 @@
}
private void upgradeToVersion308(SQLiteDatabase db) {
- db.execSQL("CREATE TABLE accounts (" +
- "account_name TEXT, " +
- "account_type TEXT " +
- ");");
+ 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");
+ db.execSQL("INSERT INTO accounts " +
+ "SELECT DISTINCT account_name, account_type FROM raw_contacts");
+ }
+
+ private void upgradeToVersion311(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 rebuildNameLookup(SQLiteDatabase db) {
@@ -2397,6 +2399,20 @@
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");
+ }
+
public String extractHandleFromEmailAddress(String email) {
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
if (tokens.length == 0) {
@@ -2452,8 +2468,6 @@
"contacts_restricted_index", "10000 9000");
updateIndexStats(db, Tables.CONTACTS,
"contacts_has_phone_index", "10000 500");
- updateIndexStats(db, Tables.CONTACTS,
- "contacts_visible_index", "10000 500 1");
updateIndexStats(db, Tables.RAW_CONTACTS,
"raw_contacts_source_id_index", "10000 1 1 1");
@@ -2661,67 +2675,39 @@
* Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
*/
public void updateAllVisible() {
- SQLiteDatabase db = getWritableDatabase();
- final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
- String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)};
-
- // There are a couple questions that can be asked regarding the
- // following two update statements:
- //
- // Q: Why do we run these two queries separately? They seem like they could be combined.
- // A: This is a result of painstaking experimentation. Turns out that the most
- // important optimization is to make sure we never update a value to its current value.
- // Changing 0 to 0 is unexpectedly expensive - SQLite actually writes the unchanged
- // rows back to disk. The other consideration is that the CONTACT_IS_VISIBLE condition
- // is very complex and executing it twice in the same statement ("if contact_visible !=
- // CONTACT_IS_VISIBLE change it to CONTACT_IS_VISIBLE") is more expensive than running
- // two update statements.
- //
- // Q: How come we are using db.update instead of compiled statements?
- // A: This is a limitation of the compiled statement API. It does not return the
- // number of rows changed. As you will see later in this method we really need
- // to know how many rows have been changed.
-
- // First update contacts that are currently marked as invisible, but need to be visible
- ContentValues values = new ContentValues();
- values.put(Contacts.IN_VISIBLE_GROUP, 1);
- int countMadeVisible = db.update(Tables.CONTACTS, values,
- Contacts.IN_VISIBLE_GROUP + "=0" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1",
- selectionArgs);
-
- // Next update contacts that are currently marked as visible, but need to be invisible
- values.put(Contacts.IN_VISIBLE_GROUP, 0);
- int countMadeInvisible = db.update(Tables.CONTACTS, values,
- Contacts.IN_VISIBLE_GROUP + "=1" + " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=0",
- selectionArgs);
-
- if (countMadeVisible != 0 || countMadeInvisible != 0) {
- // TODO break out the fields (contact_in_visible_group, sort_key, sort_key_alt) into
- // a separate table.
- // Rationale: The following statement will take a very long time on
- // a large database even though we are only changing one field from 0 to 1 or from
- // 1 to 0. The reason for the slowness is that SQLite will need to write the whole
- // page even when only one bit on it changes. Changing the visibility of a
- // significant number of contacts will likely read and write almost the entire
- // raw_contacts table. So, the solution is to break out into a separate table
- // the changing field along with the sort keys used for index-based sorting.
- // That table will occupy a smaller number of pages, so rewriting it would
- // not be as expensive.
- mVisibleUpdateRawContacts.execute();
- }
+ updateContactVisibility("");
}
/**
* Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact.
*/
public void updateContactVisible(long contactId) {
- final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
- mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId);
- mVisibleSpecificUpdate.bindLong(2, contactId);
- mVisibleSpecificUpdate.execute();
+ updateContactVisibility(" AND " + Contacts._ID + "=" + contactId);
+ }
- mVisibleSpecificUpdateRawContacts.bindLong(1, contactId);
- mVisibleSpecificUpdateRawContacts.execute();
+ private void updateContactVisibility(String selection) {
+ SQLiteDatabase db = getWritableDatabase();
+
+ final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+ String[] selectionArgs = new String[]{String.valueOf(groupMembershipMimetypeId)};
+
+ // First delete what needs to be deleted, then insert what needs to be added.
+ // Since flash writes are very expensive, this approach is much better than
+ // delete-all-insert-all.
+ db.execSQL("DELETE FROM " + Tables.VISIBLE_CONTACTS +
+ " WHERE " + "_id NOT IN" +
+ "(SELECT " + Contacts._ID +
+ " FROM " + Tables.CONTACTS +
+ " WHERE (" + Clauses.CONTACT_IS_VISIBLE + ")=1) " + selection,
+ selectionArgs);
+
+ db.execSQL("INSERT INTO " + Tables.VISIBLE_CONTACTS +
+ " SELECT " + Contacts._ID +
+ " FROM " + Tables.CONTACTS +
+ " WHERE " + Contacts._ID +
+ " NOT IN " + Tables.VISIBLE_CONTACTS +
+ " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1 " + selection,
+ selectionArgs);
}
/**
@@ -2939,7 +2925,9 @@
*/
boolean hasAccessToRestrictedData() {
final PackageManager pm = mContext.getPackageManager();
- final String[] callerPackages = pm.getPackagesForUid(Binder.getCallingUid());
+ int caller = Binder.getCallingUid();
+ if (caller == 0) return true; // root can do anything
+ final String[] callerPackages = pm.getPackagesForUid(caller);
// Has restricted access if caller matches any packages
for (String callerPackage : callerPackages) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index b7085ca..40be58f 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -16,29 +16,6 @@
package com.android.providers.contacts;
-import com.android.internal.content.SyncStateContentProviderHelper;
-import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
-import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
-import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
-import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
-import com.google.android.collect.Lists;
-import com.google.android.collect.Maps;
-import com.google.android.collect.Sets;
-
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
@@ -77,8 +54,6 @@
import android.os.MemoryFile;
import android.os.RemoteException;
import android.os.SystemProperties;
-import android.pim.vcard.VCardComposer;
-import android.pim.vcard.VCardConfig;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
@@ -114,6 +89,33 @@
import android.text.TextUtils;
import android.util.Log;
+import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
+
+import com.android.internal.content.SyncStateContentProviderHelper;
+import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
+import com.android.vcard.VCardComposer.HandlerForOutputStream;
+
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -146,7 +148,6 @@
/** Default for the maximum number of returned aggregation suggestions. */
private static final int DEFAULT_MAX_SUGGESTIONS = 5;
- private static final String GOOGLE_MY_CONTACTS_GROUP_TITLE = "System Group: My Contacts";
/**
* Property key for the legacy contact import version. The need for a version
* as opposed to a boolean flag is that if we discover bugs in the contact import process,
@@ -193,6 +194,8 @@
private static final int CONTACTS_PHOTO = 1009;
private static final int CONTACTS_AS_VCARD = 1010;
private static final int CONTACTS_AS_MULTI_VCARD = 1011;
+ private static final int CONTACTS_LOOKUP_DATA = 1012;
+ private static final int CONTACTS_LOOKUP_ID_DATA = 1013;
private static final int RAW_CONTACTS = 2002;
private static final int RAW_CONTACTS_ID = 2003;
@@ -242,6 +245,33 @@
private static final int PROVIDER_STATUS = 16001;
+ private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
+ RawContactsColumns.CONCRETE_ID + "=? AND "
+ + GroupsColumns.CONCRETE_ACCOUNT_NAME
+ + "=" + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
+ + GroupsColumns.CONCRETE_ACCOUNT_TYPE
+ + "=" + RawContactsColumns.CONCRETE_ACCOUNT_TYPE
+ + " AND " + Groups.FAVORITES + " != 0";
+
+ private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID =
+ RawContactsColumns.CONCRETE_ID + "=? AND "
+ + GroupsColumns.CONCRETE_ACCOUNT_NAME + "="
+ + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AND "
+ + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
+ + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AND "
+ + Groups.AUTO_ADD + " != 0";
+
+ private static final String[] PROJECTION_GROUP_ID
+ = new String[]{Tables.GROUPS + "." + Groups._ID};
+
+ private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? "
+ + "AND " + GroupMembership.GROUP_ROW_ID + "=? "
+ + "AND " + GroupMembership.RAW_CONTACT_ID + "=?";
+
+ private static final String SELECTION_STARRED_FROM_RAW_CONTACTS =
+ "SELECT " + RawContacts.STARRED
+ + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?";
+
private interface DataContactsQuery {
public static final String TABLE = "data "
+ "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
@@ -435,9 +465,13 @@
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
AGGREGATION_SUGGESTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data",
+ CONTACTS_LOOKUP_ID_DATA);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
CONTACTS_AS_MULTI_VCARD);
@@ -528,6 +562,7 @@
sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
+ sContactsProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID);
// Handle projections for Contacts-level statuses
addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
@@ -868,6 +903,8 @@
columns.put(Groups.DELETED, Groups.DELETED);
columns.put(Groups.NOTES, Groups.NOTES);
columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
+ columns.put(Groups.FAVORITES, Groups.FAVORITES);
+ columns.put(Groups.AUTO_ADD, Groups.AUTO_ADD);
columns.put(Groups.SYNC1, Groups.SYNC1);
columns.put(Groups.SYNC2, Groups.SYNC2);
columns.put(Groups.SYNC3, Groups.SYNC3);
@@ -1688,6 +1725,16 @@
public class GroupMembershipRowHandler extends DataRowHandler {
+ private static final String SELECTION_RAW_CONTACT_ID = RawContacts._ID + "=?";
+
+ private static final String QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID =
+ "SELECT COUNT(*) FROM " + Tables.DATA + " LEFT OUTER JOIN " + Tables .GROUPS
+ + " ON " + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID
+ + "=" + GroupsColumns.CONCRETE_ID
+ + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
+ + " AND " + Tables.DATA + "." + GroupMembership.RAW_CONTACT_ID + "=?"
+ + " AND " + Groups.FAVORITES + "!=0";
+
public GroupMembershipRowHandler() {
super(GroupMembership.CONTENT_ITEM_TYPE);
}
@@ -1696,6 +1743,9 @@
public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
resolveGroupSourceIdInValues(rawContactId, db, values, true);
long dataId = super.insert(db, rawContactId, values);
+ if (hasFavoritesGroupMembership(db, rawContactId)) {
+ updateRawContactsStar(db, rawContactId, true /* starred */);
+ }
updateVisibility(rawContactId);
return dataId;
}
@@ -1704,18 +1754,46 @@
public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
boolean callerIsSyncAdapter) {
long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
+ boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
resolveGroupSourceIdInValues(rawContactId, db, values, false);
if (!super.update(db, values, c, callerIsSyncAdapter)) {
return false;
}
+ boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
+ if (wasStarred != isStarred) {
+ updateRawContactsStar(db, rawContactId, isStarred);
+ }
updateVisibility(rawContactId);
return true;
}
+ private void updateRawContactsStar(SQLiteDatabase db, long rawContactId, boolean starred) {
+ ContentValues rawContactValues = new ContentValues();
+ rawContactValues.put(RawContacts.STARRED, starred ? 1 : 0);
+ if (db.update(Tables.RAW_CONTACTS, rawContactValues, SELECTION_RAW_CONTACT_ID,
+ new String[]{Long.toString(rawContactId)}) > 0) {
+ mContactAggregator.updateStarred(rawContactId);
+ }
+ }
+
+ private boolean hasFavoritesGroupMembership(SQLiteDatabase db, long rawContactId) {
+ final long groupMembershipMimetypeId = mDbHelper
+ .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
+ boolean isStarred = 0 < DatabaseUtils
+ .longForQuery(db, QUERY_COUNT_FAVORITES_GROUP_MEMBERSHIPS_BY_RAW_CONTACT_ID,
+ new String[]{Long.toString(groupMembershipMimetypeId), Long.toString(rawContactId)});
+ return isStarred;
+ }
+
@Override
public int delete(SQLiteDatabase db, Cursor c) {
long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
+ boolean wasStarred = hasFavoritesGroupMembership(db, rawContactId);
int count = super.delete(db, c);
+ boolean isStarred = hasFavoritesGroupMembership(db, rawContactId);
+ if (wasStarred && !isStarred) {
+ updateRawContactsStar(db, rawContactId, false /* starred */);
+ }
updateVisibility(rawContactId);
return count;
}
@@ -2396,7 +2474,7 @@
}
case RAW_CONTACTS: {
- id = insertRawContact(uri, values);
+ id = insertRawContact(uri, values, callerIsSyncAdapter);
mSyncToNetwork |= !callerIsSyncAdapter;
break;
}
@@ -2523,9 +2601,10 @@
*
* @param uri the values for the new row
* @param values the account this contact should be associated with. may be null.
+ * @param callerIsSyncAdapter
* @return the row ID of the newly created row
*/
- private long insertRawContact(Uri uri, ContentValues values) {
+ private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
mValues.clear();
mValues.putAll(values);
mValues.putNull(RawContacts.CONTACT_ID);
@@ -2547,9 +2626,69 @@
// Trigger creation of a Contact based on this RawContact at the end of transaction
mInsertedRawContacts.put(rawContactId, account);
+ if (!callerIsSyncAdapter) {
+ addAutoAddMembership(rawContactId);
+ final Long starred = values.getAsLong(RawContacts.STARRED);
+ if (starred != null && starred != 0) {
+ updateFavoritesMembership(rawContactId, starred != 0);
+ }
+ }
+
return rawContactId;
}
+ private void addAutoAddMembership(long rawContactId) {
+ final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID,
+ rawContactId);
+ if (groupId != null) {
+ insertDataGroupMembership(rawContactId, groupId);
+ }
+ }
+
+ private Long findGroupByRawContactId(String selection, long rawContactId) {
+ Cursor c = mDb.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS, PROJECTION_GROUP_ID,
+ selection,
+ new String[]{Long.toString(rawContactId)},
+ null /* groupBy */, null /* having */, null /* orderBy */);
+ try {
+ while (c.moveToNext()) {
+ return c.getLong(0);
+ }
+ return null;
+ } finally {
+ c.close();
+ }
+ }
+
+ private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
+ final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID,
+ rawContactId);
+ if (groupId != null) {
+ if (isStarred) {
+ insertDataGroupMembership(rawContactId, groupId);
+ } else {
+ deleteDataGroupMembership(rawContactId, groupId);
+ }
+ }
+ }
+
+ private void insertDataGroupMembership(long rawContactId, long groupId) {
+ ContentValues groupMembershipValues = new ContentValues();
+ groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
+ groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
+ groupMembershipValues.put(DataColumns.MIMETYPE_ID,
+ mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
+ mDb.insert(Tables.DATA, null, groupMembershipValues);
+ }
+
+ private void deleteDataGroupMembership(long rawContactId, long groupId) {
+ final String[] selectionArgs = {
+ Long.toString(mDbHelper.getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
+ Long.toString(groupId),
+ Long.toString(rawContactId)};
+ mDb.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
+ }
+
/**
* Inserts an item in the data table
*
@@ -2986,12 +3125,41 @@
}
mValues.remove(Groups.RES_PACKAGE);
+ final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null
+ ? mValues.getAsLong(Groups.FAVORITES) != 0
+ : false;
+
if (!callerIsSyncAdapter) {
mValues.put(Groups.DIRTY, 1);
}
long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
+ if (!callerIsSyncAdapter && isFavoritesGroup) {
+ // add all starred raw contacts to this group
+ String selection;
+ String[] selectionArgs;
+ if (account == null) {
+ selection = RawContacts.ACCOUNT_NAME + " IS NULL AND "
+ + RawContacts.ACCOUNT_TYPE + " IS NULL";
+ selectionArgs = null;
+ } else {
+ selection = RawContacts.ACCOUNT_NAME + "=? AND "
+ + RawContacts.ACCOUNT_TYPE + "=?";
+ selectionArgs = new String[]{account.name, account.type};
+ }
+ Cursor c = mDb.query(Tables.RAW_CONTACTS,
+ new String[]{RawContacts._ID, RawContacts.STARRED},
+ selection, selectionArgs, null, null, null);
+ while (c.moveToNext()) {
+ if (c.getLong(1) != 0) {
+ final long rawContactId = c.getLong(0);
+ insertDataGroupMembership(rawContactId, result);
+ setRawContactDirty(rawContactId);
+ }
+ }
+ }
+
if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
mVisibleTouched = true;
}
@@ -3093,7 +3261,7 @@
try {
cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
- Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID);
+ Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID);
if (cursor.moveToFirst()) {
dataId = cursor.getLong(DataContactsQuery.DATA_ID);
rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
@@ -3221,7 +3389,7 @@
case CONTACTS_ID: {
long contactId = ContentUris.parseId(uri);
- return deleteContact(contactId);
+ return deleteContact(contactId, callerIsSyncAdapter);
}
case CONTACTS_LOOKUP: {
@@ -3233,7 +3401,7 @@
}
final String lookupKey = pathSegments.get(2);
final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
- return deleteContact(contactId);
+ return deleteContact(contactId, callerIsSyncAdapter);
}
case CONTACTS_LOOKUP_ID: {
@@ -3258,7 +3426,7 @@
try {
if (c.getCount() == 1) {
// contact was unmodified so go ahead and delete it
- return deleteContact(contactId);
+ return deleteContact(contactId, callerIsSyncAdapter);
} else {
// row was changed (e.g. the merging might have changed), we got multiple
// rows or the supplied selection filtered the record out
@@ -3375,7 +3543,7 @@
return count;
}
- private int deleteContact(long contactId) {
+ private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
mSelectionArgs1[0] = Long.toString(contactId);
Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
@@ -3383,7 +3551,7 @@
try {
while (c.moveToNext()) {
long rawContactId = c.getLong(0);
- markRawContactAsDeleted(rawContactId);
+ markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
}
} finally {
c.close();
@@ -3401,7 +3569,7 @@
return count;
} else {
mDbHelper.removeContactIfSingleton(rawContactId);
- return markRawContactAsDeleted(rawContactId);
+ return markRawContactAsDeleted(rawContactId, callerIsSyncAdapter);
}
}
@@ -3416,7 +3584,7 @@
return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
}
- private int markRawContactAsDeleted(long rawContactId) {
+ private int markRawContactAsDeleted(long rawContactId, boolean callerIsSyncAdapter) {
mSyncToNetwork = true;
mValues.clear();
@@ -3425,7 +3593,7 @@
mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
mValues.putNull(RawContacts.CONTACT_ID);
mValues.put(RawContacts.DIRTY, 1);
- return updateRawContact(rawContactId, mValues);
+ return updateRawContact(rawContactId, mValues, callerIsSyncAdapter);
}
@Override
@@ -3462,12 +3630,12 @@
}
case CONTACTS: {
- count = updateContactOptions(values, selection, selectionArgs);
+ count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
break;
}
case CONTACTS_ID: {
- count = updateContactOptions(ContentUris.parseId(uri), values);
+ count = updateContactOptions(ContentUris.parseId(uri), values, callerIsSyncAdapter);
break;
}
@@ -3481,7 +3649,7 @@
}
final String lookupKey = pathSegments.get(2);
final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
- count = updateContactOptions(contactId, values);
+ count = updateContactOptions(contactId, values, callerIsSyncAdapter);
break;
}
@@ -3517,7 +3685,7 @@
case RAW_CONTACTS: {
selection = appendAccountToSelection(uri, selection);
- count = updateRawContacts(values, selection, selectionArgs);
+ count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
break;
}
@@ -3526,10 +3694,12 @@
if (selection != null) {
selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
count = updateRawContacts(values, RawContacts._ID + "=?"
- + " AND(" + selection + ")", selectionArgs);
+ + " AND(" + selection + ")", selectionArgs,
+ callerIsSyncAdapter);
} else {
mSelectionArgs1[0] = String.valueOf(rawContactId);
- count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1);
+ count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
+ callerIsSyncAdapter);
}
break;
}
@@ -3692,7 +3862,8 @@
return count;
}
- private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
+ private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
+ boolean callerIsSyncAdapter) {
if (values.containsKey(RawContacts.CONTACT_ID)) {
throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
"in content values. Contact IDs are assigned automatically");
@@ -3705,7 +3876,7 @@
try {
while (cursor.moveToNext()) {
long rawContactId = cursor.getLong(0);
- updateRawContact(rawContactId, values);
+ updateRawContact(rawContactId, values, callerIsSyncAdapter);
count++;
}
} finally {
@@ -3715,7 +3886,8 @@
return count;
}
- private int updateRawContact(long rawContactId, ContentValues values) {
+ private int updateRawContact(long rawContactId, ContentValues values,
+ boolean callerIsSyncAdapter) {
final String selection = RawContacts._ID + " = ?";
mSelectionArgs1[0] = Long.toString(rawContactId);
final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
@@ -3751,8 +3923,30 @@
}
}
if (values.containsKey(RawContacts.STARRED)) {
+ if (!callerIsSyncAdapter) {
+ updateFavoritesMembership(rawContactId,
+ values.getAsLong(RawContacts.STARRED) != 0);
+ }
mContactAggregator.updateStarred(rawContactId);
+ } else {
+ // if this raw contact is being associated with an account, then update the
+ // favorites group membership based on whether or not this contact is starred.
+ // If it is starred, add a group membership, if one doesn't already exist
+ // otherwise delete any matching group memberships.
+ if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
+ boolean starred = 0 != DatabaseUtils.longForQuery(mDb,
+ SELECTION_STARRED_FROM_RAW_CONTACTS,
+ new String[]{Long.toString(rawContactId)});
+ updateFavoritesMembership(rawContactId, starred);
+ }
}
+
+ // if this raw contact is being associated with an account, then add a
+ // group membership to the group marked as AutoAdd, if any.
+ if (!callerIsSyncAdapter && values.containsKey(RawContacts.ACCOUNT_NAME)) {
+ addAutoAddMembership(rawContactId);
+ }
+
if (values.containsKey(RawContacts.SOURCE_ID)) {
mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId);
}
@@ -3834,7 +4028,7 @@
}
private int updateContactOptions(ContentValues values, String selection,
- String[] selectionArgs) {
+ String[] selectionArgs, boolean callerIsSyncAdapter) {
int count = 0;
Cursor cursor = mDb.query(mDbHelper.getContactView(),
new String[] { Contacts._ID }, selection,
@@ -3842,7 +4036,7 @@
try {
while (cursor.moveToNext()) {
long contactId = cursor.getLong(0);
- updateContactOptions(contactId, values);
+ updateContactOptions(contactId, values, callerIsSyncAdapter);
count++;
}
} finally {
@@ -3852,7 +4046,8 @@
return count;
}
- private int updateContactOptions(long contactId, ContentValues values) {
+ private int updateContactOptions(long contactId, ContentValues values,
+ boolean callerIsSyncAdapter) {
mValues.clear();
ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
@@ -3879,6 +4074,21 @@
mSelectionArgs1[0] = String.valueOf(contactId);
mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
+ if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) {
+ Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
+ new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
+ mSelectionArgs1, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ long rawContactId = cursor.getLong(0);
+ updateFavoritesMembership(rawContactId,
+ mValues.getAsLong(RawContacts.STARRED) != 0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
// Copy changeable values to prevent automatically managed fields from
// being explicitly updated by clients.
mValues.clear();
@@ -3949,44 +4159,18 @@
return 1;
}
- /**
- * Check whether GOOGLE_MY_CONTACTS_GROUP exists, otherwise create it.
- *
- * @return the group id
- */
- private long getOrCreateMyContactsGroupInTransaction(String accountName, String accountType) {
- Cursor cursor = mDb.query(Tables.GROUPS, new String[] {"_id"},
- Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =? AND "
- + Groups.TITLE + " =?",
- new String[] {accountName, accountType, GOOGLE_MY_CONTACTS_GROUP_TITLE},
- null, null, null);
- try {
- if(cursor.moveToNext()) {
- return cursor.getLong(0);
- }
- } finally {
- cursor.close();
- }
-
- ContentValues values = new ContentValues();
- values.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP_TITLE);
- values.put(Groups.ACCOUNT_NAME, accountName);
- values.put(Groups.ACCOUNT_TYPE, accountType);
- values.put(Groups.GROUP_VISIBLE, "1");
- return mDb.insert(Tables.GROUPS, null, values);
- }
-
public void onAccountsUpdated(Account[] accounts) {
// TODO : Check the unit test.
+ boolean accountsChanged = false;
HashSet<Account> existingAccounts = new HashSet<Account>();
- boolean hasUnassignedContacts[] = new boolean[]{false};
mDb.beginTransaction();
try {
- findValidAccounts(existingAccounts, hasUnassignedContacts);
+ findValidAccounts(existingAccounts);
// Add a row to the ACCOUNTS table for each new account
for (Account account : accounts) {
if (!existingAccounts.contains(account)) {
+ accountsChanged = true;
mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
+ ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)",
new String[] {account.name, account.type});
@@ -4000,38 +4184,39 @@
accountsToDelete.remove(account);
}
- for (Account account : accountsToDelete) {
- Log.d(TAG, "removing data for removed account " + account);
- String[] params = new String[] {account.name, account.type};
- mDb.execSQL(
- "DELETE FROM " + Tables.GROUPS +
- " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
- " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
- mDb.execSQL(
- "DELETE FROM " + Tables.PRESENCE +
- " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
- "SELECT " + RawContacts._ID +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
- " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
- mDb.execSQL(
- "DELETE FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
- " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
- mDb.execSQL(
- "DELETE FROM " + Tables.SETTINGS +
- " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
- " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
- mDb.execSQL(
- "DELETE FROM " + Tables.ACCOUNTS +
- " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
- " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
- }
-
if (!accountsToDelete.isEmpty()) {
+ accountsChanged = true;
+ for (Account account : accountsToDelete) {
+ Log.d(TAG, "removing data for removed account " + account);
+ String[] params = new String[] {account.name, account.type};
+ mDb.execSQL(
+ "DELETE FROM " + Tables.GROUPS +
+ " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
+ " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
+ mDb.execSQL(
+ "DELETE FROM " + Tables.PRESENCE +
+ " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
+ "SELECT " + RawContacts._ID +
+ " FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
+ " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
+ mDb.execSQL(
+ "DELETE FROM " + Tables.RAW_CONTACTS +
+ " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
+ " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
+ mDb.execSQL(
+ "DELETE FROM " + Tables.SETTINGS +
+ " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
+ " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
+ mDb.execSQL(
+ "DELETE FROM " + Tables.ACCOUNTS +
+ " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
+ " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
+ }
+
// Find all aggregated contacts that used to contain the raw contacts
// we have just deleted and see if they are still referencing the deleted
- // names of photos. If so, fix up those contacts.
+ // names or photos. If so, fix up those contacts.
HashSet<Long> orphanContactIds = Sets.newHashSet();
Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID +
" FROM " + Tables.CONTACTS +
@@ -4054,64 +4239,12 @@
for (Long contactId : orphanContactIds) {
mContactAggregator.updateAggregateData(contactId);
}
+ mDbHelper.updateAllVisible();
}
- if (hasUnassignedContacts[0]) {
-
- Account primaryAccount = null;
- for (Account account : accounts) {
- if (isWritableAccount(account.type)) {
- primaryAccount = account;
- break;
- }
- }
-
- if (primaryAccount != null) {
- String[] params = new String[] {primaryAccount.name, primaryAccount.type};
- if (primaryAccount.type.equals(DEFAULT_ACCOUNT_TYPE)) {
- long groupId = getOrCreateMyContactsGroupInTransaction(
- primaryAccount.name, primaryAccount.type);
- if (groupId != -1) {
- long mimeTypeId = mDbHelper.getMimeTypeId(
- GroupMembership.CONTENT_ITEM_TYPE);
- mDb.execSQL(
- "INSERT INTO " + Tables.DATA + "(" + DataColumns.MIMETYPE_ID +
- ", " + Data.RAW_CONTACT_ID + ", "
- + GroupMembership.GROUP_ROW_ID + ") " +
- "SELECT " + mimeTypeId + ", "
- + RawContacts._ID + ", " + groupId +
- " FROM " + Tables.RAW_CONTACTS +
- " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
- " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"
- );
- }
- }
- mDb.execSQL(
- "UPDATE " + Tables.RAW_CONTACTS +
- " SET " + RawContacts.ACCOUNT_NAME + "=?,"
- + RawContacts.ACCOUNT_TYPE + "=?" +
- " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
- " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params);
-
- // We don't currently support groups for unsynced accounts, so this is for
- // the future
- mDb.execSQL(
- "UPDATE " + Tables.GROUPS +
- " SET " + Groups.ACCOUNT_NAME + "=?,"
- + Groups.ACCOUNT_TYPE + "=?" +
- " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" +
- " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params);
-
- mDb.execSQL(
- "DELETE FROM " + Tables.ACCOUNTS +
- " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
- " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL");
- }
+ if (accountsChanged) {
+ mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
}
-
- mDbHelper.updateAllVisible();
-
- mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
mDb.setTransactionSuccessful();
} finally {
mDb.endTransaction();
@@ -4122,15 +4255,13 @@
/**
* Finds all distinct accounts present in the specified table.
*/
- private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts) {
+ private void findValidAccounts(Set<Account> validAccounts) {
Cursor c = mDb.rawQuery(
"SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
" FROM " + Tables.ACCOUNTS, null);
try {
while (c.moveToNext()) {
- if (c.isNull(0) && c.isNull(1)) {
- hasUnassignedContacts[0] = true;
- } else {
+ if (!c.isNull(0) || !c.isNull(1)) {
validAccounts.add(new Account(c.getString(0), c.getString(1)));
}
}
@@ -4237,6 +4368,48 @@
break;
}
+ case CONTACTS_LOOKUP_DATA:
+ case CONTACTS_LOOKUP_ID_DATA: {
+ List<String> pathSegments = uri.getPathSegments();
+ int segmentCount = pathSegments.size();
+ if (segmentCount < 4) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "Missing a lookup key", uri));
+ }
+ String lookupKey = pathSegments.get(2);
+ if (segmentCount == 5) {
+ long contactId = Long.parseLong(pathSegments.get(3));
+ SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
+ setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
+ String[] args;
+ if (selectionArgs == null) {
+ args = new String[2];
+ } else {
+ args = new String[selectionArgs.length + 2];
+ System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
+ }
+ args[0] = String.valueOf(contactId);
+ args[1] = Uri.encode(lookupKey);
+ lookupQb.appendWhere(" AND " + Data.CONTACT_ID + "=?"
+ + " AND " + Data.LOOKUP_KEY + "=?");
+ Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
+ groupBy, limit);
+ if (c.getCount() != 0) {
+ return c;
+ }
+
+ c.close();
+
+ // TODO see if the contact exists but has no data rows (rare)
+ }
+
+ setTablesAndProjectionMapForData(qb, uri, projection, false);
+ selectionArgs = insertSelectionArg(selectionArgs,
+ String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
+ qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
+ break;
+ }
+
case CONTACTS_AS_VCARD: {
// When reading as vCard always use restricted view
final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
@@ -5147,25 +5320,30 @@
") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE);
}
- sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
- " WHERE " + DataColumns.CONCRETE_ID +
- " IN (");
+ sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS + " WHERE ");
- // Construct a query that gives us exactly one data _id per matching contact.
- // MIN stands in for ANY in this context.
- sb.append(
- "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
- " FROM " + Tables.NAME_LOOKUP +
- " JOIN " + Tables.RAW_CONTACTS +
- " ON (" + RawContactsColumns.CONCRETE_ID
- + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" +
- " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
- sb.append(NameNormalizer.normalize(filter));
- sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
- " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
- " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID);
+ if (!TextUtils.isEmpty(filter)) {
+ sb.append(DataColumns.CONCRETE_ID + " IN (");
- sb.append(")) ON (" + Contacts._ID + "=snippet_contact_id)");
+ // Construct a query that gives us exactly one data _id per matching contact.
+ // MIN stands in for ANY in this context.
+ sb.append(
+ "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
+ " FROM " + Tables.NAME_LOOKUP +
+ " JOIN " + Tables.RAW_CONTACTS +
+ " ON (" + RawContactsColumns.CONCRETE_ID
+ + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" +
+ " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
+ sb.append(NameNormalizer.normalize(filter));
+ sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
+ " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
+ " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID +
+ ")");
+ } else {
+ sb.append("0"); // Empty filter - return an empty set
+ }
+
+ sb.append(") ON (" + Contacts._ID + "=snippet_contact_id)");
qb.setTables(sb.toString());
qb.setProjectionMap(sContactsProjectionWithSnippetMap);
diff --git a/src/com/android/providers/contacts/GlobalSearchSupport.java b/src/com/android/providers/contacts/GlobalSearchSupport.java
index d890310..96bd39c 100644
--- a/src/com/android/providers/contacts/GlobalSearchSupport.java
+++ b/src/com/android/providers/contacts/GlobalSearchSupport.java
@@ -27,6 +27,7 @@
import android.content.ContentUris;
import android.content.res.Resources;
import android.database.Cursor;
+import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.provider.Contacts.Intents;
@@ -74,7 +75,8 @@
private interface SearchSuggestionQuery {
public static final String TABLE = "data "
+ " JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
- + " JOIN contacts ON (raw_contacts.contact_id = contacts._id)"
+ + " JOIN visible_contacts on (raw_contacts.contact_id = visible_contacts._id) "
+ + " JOIN contacts ON (visible_contacts._id = contacts._id) "
+ " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON ("
+ Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
@@ -342,18 +344,6 @@
appendMimeTypeFilter(sb);
sb.append(" AND " + DataColumns.CONCRETE_RAW_CONTACT_ID + " IN ");
mContactsProvider.appendRawContactsByFilterAsNestedQuery(sb, searchClause);
-
- /*
- * Prepending "+" to the IN_VISIBLE_GROUP column disables the index on the
- * that column. The logic is this: let's say we have 10,000 contacts
- * of which 500 are visible. The first letter we type narrows this down
- * to 10,000/26 = 384, which is already less than 500 that we would get
- * from the IN_VISIBLE_GROUP index. Typing the second letter will narrow
- * the search down to 10,000/26/26 = 14 contacts. And a lot of people
- * will have more that 5% of their contacts visible, while the alphabet
- * will always have 26 letters.
- */
- sb.append(" AND " + "+" + Contacts.IN_VISIBLE_GROUP + "=1");
String selection = sb.toString();
return buildCursorForSearchSuggestions(db, selection, null, limit);
diff --git a/src/com/android/providers/contacts/LegacyContactImporter.java b/src/com/android/providers/contacts/LegacyContactImporter.java
index 2634d44..ae89cc8 100644
--- a/src/com/android/providers/contacts/LegacyContactImporter.java
+++ b/src/com/android/providers/contacts/LegacyContactImporter.java
@@ -399,9 +399,8 @@
RawContacts.ACCOUNT_NAME + "," +
RawContacts.ACCOUNT_TYPE + "," +
RawContacts.SOURCE_ID + "," +
- RawContactsColumns.DISPLAY_NAME + "," +
- RawContactsColumns.CONTACT_IN_VISIBLE_GROUP +
- ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
+ RawContactsColumns.DISPLAY_NAME +
+ ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
int ID = 1;
int CONTACT_ID = 2;
@@ -417,7 +416,6 @@
int ACCOUNT_TYPE = 12;
int SOURCE_ID = 13;
int DISPLAY_NAME = 14;
- int CONTACT_IN_VISIBLE_GROUP = 15;
}
private interface ContactsInsert {
@@ -428,9 +426,8 @@
Contacts.SEND_TO_VOICEMAIL + "," +
Contacts.STARRED + "," +
Contacts.TIMES_CONTACTED + "," +
- Contacts.NAME_RAW_CONTACT_ID + "," +
- Contacts.IN_VISIBLE_GROUP +
- ") VALUES (?,?,?,?,?,?,?,?)";
+ Contacts.NAME_RAW_CONTACT_ID +
+ ") VALUES (?,?,?,?,?,?,?)";
int ID = 1;
int CUSTOM_RINGTONE = 2;
@@ -439,7 +436,6 @@
int STARRED = 5;
int TIMES_CONTACTED = 6;
int NAME_RAW_CONTACT_ID = 7;
- int IN_VISIBLE_GROUP = 8;
}
private interface StructuredNameInsert {
@@ -555,7 +551,6 @@
c.getString(PeopleQuery._SYNC_LOCAL_ID));
bindString(insert, RawContactsInsert.DISPLAY_NAME,
c.getString(PeopleQuery.NAME));
- insert.bindLong(RawContactsInsert.CONTACT_IN_VISIBLE_GROUP, 1);
String account = c.getString(PeopleQuery._SYNC_ACCOUNT);
if (!TextUtils.isEmpty(account)) {
@@ -585,7 +580,6 @@
insert.bindLong(ContactsInsert.TIMES_CONTACTED,
c.getLong(PeopleQuery.TIMES_CONTACTED));
insert.bindLong(ContactsInsert.NAME_RAW_CONTACT_ID, id);
- insert.bindLong(ContactsInsert.IN_VISIBLE_GROUP, 1);
insert(insert);
}
diff --git a/src/com/android/providers/contacts/NameNormalizer.java b/src/com/android/providers/contacts/NameNormalizer.java
index f40a632..6dfe8bd 100644
--- a/src/com/android/providers/contacts/NameNormalizer.java
+++ b/src/com/android/providers/contacts/NameNormalizer.java
@@ -16,8 +16,10 @@
package com.android.providers.contacts;
import com.ibm.icu4jni.text.CollationAttribute;
+import com.ibm.icu4jni.text.CollationKey; // TODO: java.text.CollationKey post-froyo
import com.ibm.icu4jni.text.Collator;
import com.ibm.icu4jni.text.RuleBasedCollator;
+import java.util.Locale;
/**
* Converts a name to a normalized form by removing all non-letter characters and normalizing
@@ -27,14 +29,14 @@
private static final RuleBasedCollator sCompressingCollator;
static {
- sCompressingCollator = (RuleBasedCollator)Collator.getInstance(null);
+ sCompressingCollator = (RuleBasedCollator)Collator.getInstance(Locale.getDefault());
sCompressingCollator.setStrength(Collator.PRIMARY);
sCompressingCollator.setDecomposition(Collator.CANONICAL_DECOMPOSITION);
}
private static final RuleBasedCollator sComplexityCollator;
static {
- sComplexityCollator = (RuleBasedCollator)Collator.getInstance(null);
+ sComplexityCollator = (RuleBasedCollator)Collator.getInstance(Locale.getDefault());
sComplexityCollator.setStrength(Collator.TERTIARY);
sComplexityCollator.setAttribute(CollationAttribute.CASE_FIRST,
CollationAttribute.VALUE_LOWER_FIRST);
@@ -45,7 +47,8 @@
* of names. It ignores non-letter characters and removes accents.
*/
public static String normalize(String name) {
- return Hex.encodeHex(sCompressingCollator.getSortKey(lettersAndDigitsOnly(name)), true);
+ CollationKey key = sCompressingCollator.getCollationKey(lettersAndDigitsOnly(name));
+ return Hex.encodeHex(key.toByteArray(), true);
}
/**
diff --git a/src/com/android/providers/contacts/ReorderingCursorWrapper.java b/src/com/android/providers/contacts/ReorderingCursorWrapper.java
index d332fa3..e52b095 100644
--- a/src/com/android/providers/contacts/ReorderingCursorWrapper.java
+++ b/src/com/android/providers/contacts/ReorderingCursorWrapper.java
@@ -94,6 +94,11 @@
}
@Override
+ public int getType(int column) {
+ return mCursor.getType(column);
+ }
+
+ @Override
public boolean isNull(int column) {
return mCursor.isNull(column);
}
diff --git a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
index de410a9..09c223a 100644
--- a/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/BaseContactsProvider2Test.java
@@ -171,24 +171,46 @@
protected long createRawContact(Account account, String... extras) {
ContentValues values = new ContentValues();
- for (int i = 0; i < extras.length; ) {
- values.put(extras[i], extras[i + 1]);
- i += 2;
- }
+ extrasVarArgsToValues(values, extras);
final Uri uri = maybeAddAccountQueryParameters(RawContacts.CONTENT_URI, account);
Uri contactUri = mResolver.insert(uri, values);
return ContentUris.parseId(contactUri);
}
+ protected int updateItem(Uri uri, long id, String... extras) {
+ Uri itemUri = ContentUris.withAppendedId(uri, id);
+ return updateItem(itemUri, extras);
+ }
+
+ protected int updateItem(Uri uri, String... extras) {
+ ContentValues values = new ContentValues();
+ extrasVarArgsToValues(values, extras);
+ return mResolver.update(uri, values, null, null);
+ }
+
+ private static void extrasVarArgsToValues(ContentValues values, String... extras) {
+ for (int i = 0; i < extras.length; ) {
+ values.put(extras[i], extras[i + 1]);
+ i += 2;
+ }
+ }
+
protected long createGroup(Account account, String sourceId, String title) {
- return createGroup(account, sourceId, title, 1);
+ return createGroup(account, sourceId, title, 1, false, false);
}
protected long createGroup(Account account, String sourceId, String title, int visible) {
+ return createGroup(account, sourceId, title, visible, false, false);
+ }
+
+ protected long createGroup(Account account, String sourceId, String title,
+ int visible, boolean autoAdd, boolean favorite) {
ContentValues values = new ContentValues();
values.put(Groups.SOURCE_ID, sourceId);
values.put(Groups.TITLE, title);
values.put(Groups.GROUP_VISIBLE, visible);
+ values.put(Groups.AUTO_ADD, autoAdd ? 1 : 0);
+ values.put(Groups.FAVORITES, favorite ? 1 : 0);
final Uri uri = maybeAddAccountQueryParameters(Groups.CONTENT_URI, account);
return ContentUris.parseId(mResolver.insert(uri, values));
}
@@ -419,6 +441,16 @@
return photoId;
}
+ protected boolean queryRawContactIsStarred(long rawContactId) {
+ Cursor c = queryRawContact(rawContactId);
+ try {
+ assertTrue(c.moveToFirst());
+ return c.getLong(c.getColumnIndex(RawContacts.STARRED)) != 0;
+ } finally {
+ c.close();
+ }
+ }
+
protected String queryDisplayName(long contactId) {
Cursor c = queryContact(contactId);
assertTrue(c.moveToFirst());
@@ -427,7 +459,7 @@
return displayName;
}
- private String queryLookupKey(long contactId) {
+ protected String queryLookupKey(long contactId) {
Cursor c = queryContact(contactId);
assertTrue(c.moveToFirst());
String lookupKey = c.getString(c.getColumnIndex(Contacts.LOOKUP_KEY));
@@ -577,6 +609,14 @@
}
}
+ protected void assertNoRowsAndClose(Cursor c) {
+ try {
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+ }
+
protected static class IdComparator implements Comparator<ContentValues> {
public int compare(ContentValues o1, ContentValues o2) {
long id1 = o1.getAsLong(ContactsContract.Data._ID);
diff --git a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
index a8beec0..f6b4708 100644
--- a/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
+++ b/tests/src/com/android/providers/contacts/ContactsProvider2Test.java
@@ -106,6 +106,51 @@
assertNetworkNotified(true);
}
+ public void testDataDirectoryWithLookupUri() {
+ ContentValues values = new ContentValues();
+
+ long rawContactId = createRawContactWithName();
+ insertPhoneNumber(rawContactId, "555-GOOG-411");
+ insertEmail(rawContactId, "google@android.com");
+
+ long contactId = queryContactId(rawContactId);
+ String lookupKey = queryLookupKey(contactId);
+
+ // Complete and valid lookup URI
+ Uri lookupUri = ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
+ Uri dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ assertDataRows(dataUri, values);
+
+ // Complete but stale lookup URI
+ lookupUri = ContactsContract.Contacts.getLookupUri(contactId + 1, lookupKey);
+ dataUri = Uri.withAppendedPath(lookupUri, Contacts.Data.CONTENT_DIRECTORY);
+ assertDataRows(dataUri, values);
+
+ // Incomplete lookup URI (lookup key only, no contact ID)
+ dataUri = Uri.withAppendedPath(Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI,
+ lookupKey), Contacts.Data.CONTENT_DIRECTORY);
+ assertDataRows(dataUri, values);
+ }
+
+ private void assertDataRows(Uri dataUri, ContentValues values) {
+ Cursor cursor = mResolver.query(dataUri, new String[]{ Data.DATA1 }, null, null, Data._ID);
+ assertEquals(3, cursor.getCount());
+ cursor.moveToFirst();
+ values.put(Data.DATA1, "John Doe");
+ assertCursorValues(cursor, values);
+
+ cursor.moveToNext();
+ values.put(Data.DATA1, "555-GOOG-411");
+ assertCursorValues(cursor, values);
+
+ cursor.moveToNext();
+ values.put(Data.DATA1, "google@android.com");
+ assertCursorValues(cursor, values);
+
+ cursor.close();
+ }
+
public void testDataInsert() {
long rawContactId = createRawContactWithName("John", "Doe");
@@ -1738,8 +1783,8 @@
ContactsProvider2 cp = (ContactsProvider2) getProvider();
cp.onAccountsUpdated(new Account[]{mAccount, mAccountTwo});
assertEquals(1, getCount(RawContacts.CONTENT_URI, null, null));
- assertStoredValue(rawContact3, RawContacts.ACCOUNT_NAME, "account1");
- assertStoredValue(rawContact3, RawContacts.ACCOUNT_TYPE, "account type1");
+ assertStoredValue(rawContact3, RawContacts.ACCOUNT_NAME, null);
+ assertStoredValue(rawContact3, RawContacts.ACCOUNT_TYPE, null);
long rawContactId1 = createRawContact(mAccount);
insertEmail(rawContactId1, "account1@email.com");
@@ -2409,6 +2454,285 @@
}
}
+ public void testAutoGroupMembership() {
+ long g1 = createGroup(mAccount, "g1", "t1", 0, true /* autoAdd */, false /* favorite */);
+ long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false /* favorite */);
+ long g3 = createGroup(mAccountTwo, "g3", "t3", 0, true /* autoAdd */, false /* favorite */);
+ long g4 = createGroup(mAccountTwo, "g4", "t4", 0, false /* autoAdd */, false/* favorite */);
+ long r1 = createRawContact(mAccount);
+ long r2 = createRawContact(mAccountTwo);
+ long r3 = createRawContact(null);
+
+ Cursor c = queryGroupMemberships(mAccount);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r1, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ c = queryGroupMemberships(mAccountTwo);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g3, c.getLong(0));
+ assertEquals(r2, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+ }
+
+ public void testNoAutoAddMembershipAfterGroupCreation() {
+ long r1 = createRawContact(mAccount);
+ long r2 = createRawContact(mAccount);
+ long r3 = createRawContact(mAccount);
+ long r4 = createRawContact(mAccountTwo);
+ long r5 = createRawContact(mAccountTwo);
+ long r6 = createRawContact(null);
+
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+ long g1 = createGroup(mAccount, "g1", "t1", 0, true /* autoAdd */, false /* favorite */);
+ long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false /* favorite */);
+ long g3 = createGroup(mAccountTwo, "g3", "t3", 0, true /* autoAdd */, false/* favorite */);
+
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ }
+
+ // create some starred and non-starred contacts, some associated with account, some not
+ // favorites group created
+ // the starred contacts should be added to group
+ // favorites group removed
+ // no change to starred status
+ public void testFavoritesMembershipAfterGroupCreation() {
+ long r1 = createRawContact(mAccount, RawContacts.STARRED, "1");
+ long r2 = createRawContact(mAccount);
+ long r3 = createRawContact(mAccount, RawContacts.STARRED, "1");
+ long r4 = createRawContact(mAccountTwo, RawContacts.STARRED, "1");
+ long r5 = createRawContact(mAccountTwo);
+ long r6 = createRawContact(null, RawContacts.STARRED, "1");
+ long r7 = createRawContact(null);
+
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+ long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
+ long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false /* favorite */);
+ long g3 = createGroup(mAccountTwo, "g3", "t3", 0, false /* autoAdd */, false/* favorite */);
+
+ assertTrue(queryRawContactIsStarred(r1));
+ assertFalse(queryRawContactIsStarred(r2));
+ assertTrue(queryRawContactIsStarred(r3));
+ assertTrue(queryRawContactIsStarred(r4));
+ assertFalse(queryRawContactIsStarred(r5));
+ assertTrue(queryRawContactIsStarred(r6));
+ assertFalse(queryRawContactIsStarred(r7));
+
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ Cursor c = queryGroupMemberships(mAccount);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r1, c.getLong(1));
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r3, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ updateItem(RawContacts.CONTENT_URI, r6,
+ RawContacts.ACCOUNT_NAME, mAccount.name,
+ RawContacts.ACCOUNT_TYPE, mAccount.type);
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ c = queryGroupMemberships(mAccount);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r1, c.getLong(1));
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r3, c.getLong(1));
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r6, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ mResolver.delete(ContentUris.withAppendedId(Groups.CONTENT_URI, g1), null, null);
+
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+ assertTrue(queryRawContactIsStarred(r1));
+ assertFalse(queryRawContactIsStarred(r2));
+ assertTrue(queryRawContactIsStarred(r3));
+ assertTrue(queryRawContactIsStarred(r4));
+ assertFalse(queryRawContactIsStarred(r5));
+ assertTrue(queryRawContactIsStarred(r6));
+ assertFalse(queryRawContactIsStarred(r7));
+ }
+
+ public void testFavoritesGroupMembershipChangeAfterStarChange() {
+ long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
+ long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false/* favorite */);
+ long g4 = createGroup(mAccountTwo, "g4", "t4", 0, false /* autoAdd */, true /* favorite */);
+ long g5 = createGroup(mAccountTwo, "g5", "t5", 0, false /* autoAdd */, false/* favorite */);
+ long r1 = createRawContact(mAccount, RawContacts.STARRED, "1");
+ long r2 = createRawContact(mAccount);
+ long r3 = createRawContact(mAccountTwo);
+
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ Cursor c = queryGroupMemberships(mAccount);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r1, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ // remove the star from r1
+ assertEquals(1, updateItem(RawContacts.CONTENT_URI, r1, RawContacts.STARRED, "0"));
+
+ // Since no raw contacts are starred, there should be no group memberships.
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+ // mark r1 as starred
+ assertEquals(1, updateItem(RawContacts.CONTENT_URI, r1, RawContacts.STARRED, "1"));
+ // Now that r1 is starred it should have a membership in the one groups from mAccount
+ // that is marked as a favorite.
+ // There should be no memberships in mAccountTwo since it has no starred raw contacts.
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ c = queryGroupMemberships(mAccount);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r1, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ // remove the star from r1
+ assertEquals(1, updateItem(RawContacts.CONTENT_URI, r1, RawContacts.STARRED, "0"));
+ // Since no raw contacts are starred, there should be no group memberships.
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+ Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, queryContactId(r1));
+ assertNotNull(contactUri);
+
+ // mark r1 as starred via its contact lookup uri
+ assertEquals(1, updateItem(contactUri, Contacts.STARRED, "1"));
+ // Now that r1 is starred it should have a membership in the one groups from mAccount
+ // that is marked as a favorite.
+ // There should be no memberships in mAccountTwo since it has no starred raw contacts.
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ c = queryGroupMemberships(mAccount);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r1, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ // remove the star from r1
+ updateItem(contactUri, Contacts.STARRED, "0");
+ // Since no raw contacts are starred, there should be no group memberships.
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ }
+
+ public void testStarChangedAfterGroupMembershipChange() {
+ long g1 = createGroup(mAccount, "g1", "t1", 0, false /* autoAdd */, true /* favorite */);
+ long g2 = createGroup(mAccount, "g2", "t2", 0, false /* autoAdd */, false/* favorite */);
+ long g4 = createGroup(mAccountTwo, "g4", "t4", 0, false /* autoAdd */, true /* favorite */);
+ long g5 = createGroup(mAccountTwo, "g5", "t5", 0, false /* autoAdd */, false/* favorite */);
+ long r1 = createRawContact(mAccount);
+ long r2 = createRawContact(mAccount);
+ long r3 = createRawContact(mAccountTwo);
+
+ assertFalse(queryRawContactIsStarred(r1));
+ assertFalse(queryRawContactIsStarred(r2));
+ assertFalse(queryRawContactIsStarred(r3));
+
+ Cursor c;
+
+ // add r1 to one favorites group
+ // r1's star should automatically be set
+ // r1 should automatically be added to the other favorites group
+ Uri urir1g1 = insertGroupMembership(r1, g1);
+ assertTrue(queryRawContactIsStarred(r1));
+ assertFalse(queryRawContactIsStarred(r2));
+ assertFalse(queryRawContactIsStarred(r3));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ c = queryGroupMemberships(mAccount);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g1, c.getLong(0));
+ assertEquals(r1, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ // remove r1 from one favorites group
+ mResolver.delete(urir1g1, null, null);
+ // r1's star should no longer be set
+ assertFalse(queryRawContactIsStarred(r1));
+ assertFalse(queryRawContactIsStarred(r2));
+ assertFalse(queryRawContactIsStarred(r3));
+ // there should be no membership rows
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+
+ // add r3 to the one favorites group for that account
+ // r3's star should automatically be set
+ Uri urir3g4 = insertGroupMembership(r3, g4);
+ assertFalse(queryRawContactIsStarred(r1));
+ assertFalse(queryRawContactIsStarred(r2));
+ assertTrue(queryRawContactIsStarred(r3));
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ c = queryGroupMemberships(mAccountTwo);
+ try {
+ assertTrue(c.moveToNext());
+ assertEquals(g4, c.getLong(0));
+ assertEquals(r3, c.getLong(1));
+ assertFalse(c.moveToNext());
+ } finally {
+ c.close();
+ }
+
+ // remove r3 from the favorites group
+ mResolver.delete(urir3g4, null, null);
+ // r3's star should automatically be cleared
+ assertFalse(queryRawContactIsStarred(r1));
+ assertFalse(queryRawContactIsStarred(r2));
+ assertFalse(queryRawContactIsStarred(r3));
+ assertNoRowsAndClose(queryGroupMemberships(mAccount));
+ assertNoRowsAndClose(queryGroupMemberships(mAccountTwo));
+ }
+
+ private Cursor queryGroupMemberships(Account account) {
+ Cursor c = mResolver.query(maybeAddAccountQueryParameters(Data.CONTENT_URI, account),
+ new String[]{GroupMembership.GROUP_ROW_ID, GroupMembership.RAW_CONTACT_ID},
+ Data.MIMETYPE + "=?", new String[]{GroupMembership.CONTENT_ITEM_TYPE},
+ GroupMembership.GROUP_SOURCE_ID);
+ return c;
+ }
+
private String readToEnd(FileInputStream inputStream) {
try {
int ch;