Refactoring: Changed ContactLoader to be an AsyncTaskLoader

Change-Id: Iaa587907b2933aa084974e181f24121bc57b4cfb
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 685bd7d..e85f5f5 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -28,6 +28,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
+import android.content.AsyncTaskLoader;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -35,14 +36,12 @@
 import android.content.Entity;
 import android.content.Entity.NamedContentValues;
 import android.content.Intent;
-import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
@@ -70,7 +69,7 @@
 /**
  * Loads a single Contact and all it constituent RawContacts.
  */
-public class ContactLoader extends Loader<ContactLoader.Result> {
+public class ContactLoader extends AsyncTaskLoader<ContactLoader.Result> {
     private static final String TAG = "ContactLoader";
 
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -82,13 +81,26 @@
     private final boolean mLoadInvitableAccountTypes;
     private Result mContact;
     private ForceLoadContentObserver mObserver;
-    private boolean mDestroyed;
     private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
 
     public interface Listener {
         public void onContactLoaded(Result contact);
     }
 
+    public ContactLoader(Context context, Uri lookupUri) {
+        this(context, lookupUri, false, false, false);
+    }
+
+    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
+            boolean loadStreamItems, boolean loadInvitableAccountTypes) {
+        super(context);
+        mLookupUri = lookupUri;
+        mRequestedUri = lookupUri;
+        mLoadGroupMetaData = loadGroupMetaData;
+        mLoadStreamItems = loadStreamItems;
+        mLoadInvitableAccountTypes = loadInvitableAccountTypes;
+    }
+
     /**
      * The result of a load operation. Contains all data necessary to display the contact.
      */
@@ -670,498 +682,495 @@
         public final static int FAVORITES = 7;
     }
 
-    private final class LoadContactTask extends AsyncTask<Void, Void, Result> {
-
-        @Override
-        protected Result doInBackground(Void... args) {
-            try {
-                final ContentResolver resolver = getContext().getContentResolver();
-                final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
-                        resolver, mLookupUri);
-                Result result = loadContactEntity(resolver, uriCurrentFormat);
-                if (!result.isNotFound()) {
-                    if (result.isDirectoryEntry()) {
-                        loadDirectoryMetaData(result);
-                    } else if (mLoadGroupMetaData) {
-                        loadGroupMetaData(result);
-                    }
-                    if (mLoadStreamItems) {
-                        loadStreamItems(result);
-                    }
-                    loadPhotoBinaryData(result);
-
-                    // Note ME profile should never have "Add connection"
-                    if (mLoadInvitableAccountTypes && !result.isUserProfile()) {
-                        loadInvitableAccountTypes(result);
-                    }
+    @Override
+    public Result loadInBackground() {
+        try {
+            final ContentResolver resolver = getContext().getContentResolver();
+            final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(
+                    resolver, mLookupUri);
+            Result result = loadContactEntity(resolver, uriCurrentFormat);
+            if (!result.isNotFound()) {
+                if (result.isDirectoryEntry()) {
+                    loadDirectoryMetaData(result);
+                } else if (mLoadGroupMetaData) {
+                    loadGroupMetaData(result);
                 }
-                return result;
-            } catch (Exception e) {
-                Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
-                return Result.forError(mRequestedUri, e);
+                if (mLoadStreamItems) {
+                    loadStreamItems(result);
+                }
+                loadPhotoBinaryData(result);
+
+                // Note ME profile should never have "Add connection"
+                if (mLoadInvitableAccountTypes && !result.isUserProfile()) {
+                    loadInvitableAccountTypes(result);
+                }
             }
+            return result;
+        } catch (Exception e) {
+            Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
+            return Result.forError(mRequestedUri, e);
+        }
+    }
+
+    private Result loadContactEntity(ContentResolver resolver, Uri contactUri) {
+        Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
+        Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
+                Contacts.Entity.RAW_CONTACT_ID);
+        if (cursor == null) {
+            Log.e(TAG, "No cursor returned in loadContactEntity");
+            return Result.forNotFound(mRequestedUri);
         }
 
-        private Result loadContactEntity(ContentResolver resolver, Uri contactUri) {
-            Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
-            Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null,
-                    Contacts.Entity.RAW_CONTACT_ID);
-            if (cursor == null) {
-                Log.e(TAG, "No cursor returned in loadContactEntity");
+        try {
+            if (!cursor.moveToFirst()) {
+                cursor.close();
                 return Result.forNotFound(mRequestedUri);
             }
 
-            try {
-                if (!cursor.moveToFirst()) {
-                    cursor.close();
-                    return Result.forNotFound(mRequestedUri);
+            long currentRawContactId = -1;
+            Entity entity = null;
+            Result result = loadContactHeaderData(cursor, contactUri);
+            ArrayList<Entity> entities = result.getEntities();
+            LongSparseArray<DataStatus> statuses = result.getStatuses();
+            for (; !cursor.isAfterLast(); cursor.moveToNext()) {
+                long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
+                if (rawContactId != currentRawContactId) {
+                    currentRawContactId = rawContactId;
+                    entity = new android.content.Entity(loadRawContact(cursor));
+                    entities.add(entity);
                 }
+                if (!cursor.isNull(ContactQuery.DATA_ID)) {
+                    ContentValues data = loadData(cursor);
+                    entity.addSubValue(ContactsContract.Data.CONTENT_URI, data);
 
-                long currentRawContactId = -1;
-                Entity entity = null;
-                Result result = loadContactHeaderData(cursor, contactUri);
-                ArrayList<Entity> entities = result.getEntities();
-                LongSparseArray<DataStatus> statuses = result.getStatuses();
-                for (; !cursor.isAfterLast(); cursor.moveToNext()) {
-                    long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
-                    if (rawContactId != currentRawContactId) {
-                        currentRawContactId = rawContactId;
-                        entity = new android.content.Entity(loadRawContact(cursor));
-                        entities.add(entity);
-                    }
-                    if (!cursor.isNull(ContactQuery.DATA_ID)) {
-                        ContentValues data = loadData(cursor);
-                        entity.addSubValue(ContactsContract.Data.CONTENT_URI, data);
-
-                        if (!cursor.isNull(ContactQuery.PRESENCE)
-                                || !cursor.isNull(ContactQuery.STATUS)) {
-                            final DataStatus status = new DataStatus(cursor);
-                            final long dataId = cursor.getLong(ContactQuery.DATA_ID);
-                            statuses.put(dataId, status);
-                        }
+                    if (!cursor.isNull(ContactQuery.PRESENCE)
+                            || !cursor.isNull(ContactQuery.STATUS)) {
+                        final DataStatus status = new DataStatus(cursor);
+                        final long dataId = cursor.getLong(ContactQuery.DATA_ID);
+                        statuses.put(dataId, status);
                     }
                 }
-
-                return result;
-            } finally {
-                cursor.close();
             }
+
+            return result;
+        } finally {
+            cursor.close();
         }
+    }
 
-        /**
-         * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
-         * not found, returns null
-         */
-        private void loadPhotoBinaryData(Result contactData) {
+    /**
+     * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If
+     * not found, returns null
+     */
+    private void loadPhotoBinaryData(Result contactData) {
 
-            // If we have a photo URI, try loading that first.
-            String photoUri = contactData.getPhotoUri();
-            if (photoUri != null) {
+        // If we have a photo URI, try loading that first.
+        String photoUri = contactData.getPhotoUri();
+        if (photoUri != null) {
+            try {
+                AssetFileDescriptor fd = getContext().getContentResolver()
+                       .openAssetFileDescriptor(Uri.parse(photoUri), "r");
+                byte[] buffer = new byte[16 * 1024];
+                FileInputStream fis = fd.createInputStream();
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                 try {
-                    AssetFileDescriptor fd = getContext().getContentResolver()
-                           .openAssetFileDescriptor(Uri.parse(photoUri), "r");
-                    byte[] buffer = new byte[16 * 1024];
-                    FileInputStream fis = fd.createInputStream();
-                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                    int size;
+                    while ((size = fis.read(buffer)) != -1) {
+                        baos.write(buffer, 0, size);
+                    }
+                    contactData.setPhotoBinaryData(baos.toByteArray());
+                } finally {
+                    fis.close();
+                    fd.close();
+                }
+                return;
+            } catch (IOException ioe) {
+                // Just fall back to the case below.
+            }
+        }
+
+        // If we couldn't load from a file, fall back to the data blob.
+        final long photoId = contactData.getPhotoId();
+        if (photoId <= 0) {
+            // No photo ID
+            return;
+        }
+
+        for (Entity entity : contactData.getEntities()) {
+            for (NamedContentValues subValue : entity.getSubValues()) {
+                final ContentValues entryValues = subValue.values;
+                final long dataId = entryValues.getAsLong(Data._ID);
+                if (dataId == photoId) {
+                    final String mimeType = entryValues.getAsString(Data.MIMETYPE);
+                    // Correct Data Id but incorrect MimeType? Don't load
+                    if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                        return;
+                    }
+                    contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO));
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
+     */
+    private void loadInvitableAccountTypes(Result contactData) {
+        Map<AccountTypeWithDataSet, AccountType> invitables =
+                AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+        if (invitables.isEmpty()) {
+            return;
+        }
+
+        Map<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(invitables);
+
+        // Remove the ones that already have a raw contact in the current contact
+        for (Entity entity : contactData.getEntities()) {
+            final ContentValues values = entity.getEntityValues();
+            final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
+                    values.getAsString(RawContacts.ACCOUNT_TYPE),
+                    values.getAsString(RawContacts.DATA_SET));
+            result.remove(type);
+        }
+
+        // Set to mInvitableAccountTypes
+        contactData.mInvitableAccountTypes.addAll(result.values());
+    }
+
+    /**
+     * Extracts Contact level columns from the cursor.
+     */
+    private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+        final String directoryParameter =
+                contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+        final long directoryId = directoryParameter == null
+                ? Directory.DEFAULT
+                : Long.parseLong(directoryParameter);
+        final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+        final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+        final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+        final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+        final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+        final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
+        final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+        final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+        final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
+        final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+        final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+                ? null
+                : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+        final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
+        final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
+        final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
+
+        Uri lookupUri;
+        if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+            lookupUri = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
+        } else {
+            lookupUri = contactUri;
+        }
+
+        return new Result(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
+                contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
+                altDisplayName, phoneticName, starred, presence, sendToVoicemail,
+                customRingtone, isUserProfile);
+    }
+
+    /**
+     * Extracts RawContact level columns from the cursor.
+     */
+    private ContentValues loadRawContact(Cursor cursor) {
+        ContentValues cv = new ContentValues();
+
+        cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
+
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED);
+
+        return cv;
+    }
+
+    /**
+     * Extracts Data level columns from the cursor.
+     */
+    private ContentValues loadData(Cursor cursor) {
+        ContentValues cv = new ContentValues();
+
+        cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
+
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
+        cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
+
+        return cv;
+    }
+
+    private void cursorColumnToContentValues(
+            Cursor cursor, ContentValues values, int index) {
+        switch (cursor.getType(index)) {
+            case Cursor.FIELD_TYPE_NULL:
+                // don't put anything in the content values
+                break;
+            case Cursor.FIELD_TYPE_INTEGER:
+                values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
+                break;
+            case Cursor.FIELD_TYPE_STRING:
+                values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
+                break;
+            case Cursor.FIELD_TYPE_BLOB:
+                values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
+                break;
+            default:
+                throw new IllegalStateException("Invalid or unhandled data type");
+        }
+    }
+
+    private void loadDirectoryMetaData(Result result) {
+        long directoryId = result.getDirectoryId();
+
+        Cursor cursor = getContext().getContentResolver().query(
+                ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
+                DirectoryQuery.COLUMNS, null, null, null);
+        if (cursor == null) {
+            return;
+        }
+        try {
+            if (cursor.moveToFirst()) {
+                final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+                final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+                final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+                final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+                final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+                final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+                String directoryType = null;
+                if (!TextUtils.isEmpty(packageName)) {
+                    PackageManager pm = getContext().getPackageManager();
                     try {
-                        int size;
-                        while ((size = fis.read(buffer)) != -1) {
-                            baos.write(buffer, 0, size);
-                        }
-                        contactData.setPhotoBinaryData(baos.toByteArray());
-                    } finally {
-                        fis.close();
-                        fd.close();
-                    }
-                    return;
-                } catch (IOException ioe) {
-                    // Just fall back to the case below.
-                }
-            }
-
-            // If we couldn't load from a file, fall back to the data blob.
-            final long photoId = contactData.getPhotoId();
-            if (photoId <= 0) {
-                // No photo ID
-                return;
-            }
-
-            for (Entity entity : contactData.getEntities()) {
-                for (NamedContentValues subValue : entity.getSubValues()) {
-                    final ContentValues entryValues = subValue.values;
-                    final long dataId = entryValues.getAsLong(Data._ID);
-                    if (dataId == photoId) {
-                        final String mimeType = entryValues.getAsString(Data.MIMETYPE);
-                        // Correct Data Id but incorrect MimeType? Don't load
-                        if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                            return;
-                        }
-                        contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO));
-                        break;
+                        Resources resources = pm.getResourcesForApplication(packageName);
+                        directoryType = resources.getString(typeResourceId);
+                    } catch (NameNotFoundException e) {
+                        Log.w(TAG, "Contact directory resource not found: "
+                                + packageName + "." + typeResourceId);
                     }
                 }
+
+                result.setDirectoryMetaData(
+                        displayName, directoryType, accountType, accountName, exportSupport);
             }
+        } finally {
+            cursor.close();
         }
+    }
 
-        /**
-         * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
-         */
-        private void loadInvitableAccountTypes(Result contactData) {
-            Map<AccountTypeWithDataSet, AccountType> invitables =
-                    AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
-            if (invitables.isEmpty()) {
-                return;
-            }
-
-            Map<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(invitables);
-
-            // Remove the ones that already have a raw contact in the current contact
-            for (Entity entity : contactData.getEntities()) {
-                final ContentValues values = entity.getEntityValues();
-                final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
-                        values.getAsString(RawContacts.ACCOUNT_TYPE),
-                        values.getAsString(RawContacts.DATA_SET));
-                result.remove(type);
-            }
-
-            // Set to mInvitableAccountTypes
-            contactData.mInvitableAccountTypes.addAll(result.values());
-        }
-
-        /**
-         * Extracts Contact level columns from the cursor.
-         */
-        private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) {
-            final String directoryParameter =
-                    contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
-            final long directoryId = directoryParameter == null
-                    ? Directory.DEFAULT
-                    : Long.parseLong(directoryParameter);
-            final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
-            final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
-            final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
-            final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
-            final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
-            final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
-            final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
-            final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
-            final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
-            final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
-            final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
-                    ? null
-                    : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
-            final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
-            final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
-            final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
-
-            Uri lookupUri;
-            if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
-                lookupUri = ContentUris.withAppendedId(
-                    Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
-            } else {
-                lookupUri = contactUri;
-            }
-
-            return new Result(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey,
-                    contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName,
-                    altDisplayName, phoneticName, starred, presence, sendToVoicemail,
-                    customRingtone, isUserProfile);
-        }
-
-        /**
-         * Extracts RawContact level columns from the cursor.
-         */
-        private ContentValues loadRawContact(Cursor cursor) {
-            ContentValues cv = new ContentValues();
-
-            cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
-
-            cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED);
-
-            return cv;
-        }
-
-        /**
-         * Extracts Data level columns from the cursor.
-         */
-        private ContentValues loadData(Cursor cursor) {
-            ContentValues cv = new ContentValues();
-
-            cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
-
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
-            cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
-
-            return cv;
-        }
-
-        private void cursorColumnToContentValues(
-                Cursor cursor, ContentValues values, int index) {
-            switch (cursor.getType(index)) {
-                case Cursor.FIELD_TYPE_NULL:
-                    // don't put anything in the content values
-                    break;
-                case Cursor.FIELD_TYPE_INTEGER:
-                    values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
-                    break;
-                case Cursor.FIELD_TYPE_STRING:
-                    values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
-                    break;
-                case Cursor.FIELD_TYPE_BLOB:
-                    values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
-                    break;
-                default:
-                    throw new IllegalStateException("Invalid or unhandled data type");
-            }
-        }
-
-        private void loadDirectoryMetaData(Result result) {
-            long directoryId = result.getDirectoryId();
-
-            Cursor cursor = getContext().getContentResolver().query(
-                    ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
-                    DirectoryQuery.COLUMNS, null, null, null);
-            if (cursor == null) {
-                return;
-            }
-            try {
-                if (cursor.moveToFirst()) {
-                    final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
-                    final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
-                    final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
-                    final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
-                    final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
-                    final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
-                    String directoryType = null;
-                    if (!TextUtils.isEmpty(packageName)) {
-                        PackageManager pm = getContext().getPackageManager();
-                        try {
-                            Resources resources = pm.getResourcesForApplication(packageName);
-                            directoryType = resources.getString(typeResourceId);
-                        } catch (NameNotFoundException e) {
-                            Log.w(TAG, "Contact directory resource not found: "
-                                    + packageName + "." + typeResourceId);
-                        }
-                    }
-
-                    result.setDirectoryMetaData(
-                            displayName, directoryType, accountType, accountName, exportSupport);
+    /**
+     * Loads groups meta-data for all groups associated with all constituent raw contacts'
+     * accounts.
+     */
+    private void loadGroupMetaData(Result result) {
+        StringBuilder selection = new StringBuilder();
+        ArrayList<String> selectionArgs = new ArrayList<String>();
+        for (Entity entity : result.mEntities) {
+            ContentValues values = entity.getEntityValues();
+            String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
+            String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
+            String dataSet = values.getAsString(RawContacts.DATA_SET);
+            if (accountName != null && accountType != null) {
+                if (selection.length() != 0) {
+                    selection.append(" OR ");
                 }
-            } finally {
-                cursor.close();
-            }
-        }
+                selection.append(
+                        "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
+                selectionArgs.add(accountName);
+                selectionArgs.add(accountType);
 
-        /**
-         * Loads groups meta-data for all groups associated with all constituent raw contacts'
-         * accounts.
-         */
-        private void loadGroupMetaData(Result result) {
-            StringBuilder selection = new StringBuilder();
-            ArrayList<String> selectionArgs = new ArrayList<String>();
-            for (Entity entity : result.mEntities) {
-                ContentValues values = entity.getEntityValues();
-                String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
-                String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
-                String dataSet = values.getAsString(RawContacts.DATA_SET);
-                if (accountName != null && accountType != null) {
-                    if (selection.length() != 0) {
-                        selection.append(" OR ");
-                    }
-                    selection.append(
-                            "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
-                    selectionArgs.add(accountName);
-                    selectionArgs.add(accountType);
-
-                    if (dataSet != null) {
-                        selection.append(" AND " + Groups.DATA_SET + "=?");
-                        selectionArgs.add(dataSet);
-                    } else {
-                        selection.append(" AND " + Groups.DATA_SET + " IS NULL");
-                    }
-                    selection.append(")");
-                }
-            }
-            Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
-                    GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
-                    null);
-            try {
-                while (cursor.moveToNext()) {
-                    final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
-                    final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
-                    final String dataSet = cursor.getString(GroupQuery.DATA_SET);
-                    final long groupId = cursor.getLong(GroupQuery.ID);
-                    final String title = cursor.getString(GroupQuery.TITLE);
-                    final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
-                            ? false
-                            : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
-                    final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
-                            ? false
-                            : cursor.getInt(GroupQuery.FAVORITES) != 0;
-
-                    result.addGroupMetaData(new GroupMetaData(
-                            accountName, accountType, dataSet, groupId, title, defaultGroup,
-                            favorites));
-                }
-            } finally {
-                cursor.close();
-            }
-        }
-
-        /**
-         * Loads all stream items and stream item photos belonging to this contact.
-         */
-        private void loadStreamItems(Result result) {
-            Cursor cursor = getContext().getContentResolver().query(
-                    Contacts.CONTENT_LOOKUP_URI.buildUpon()
-                            .appendPath(result.getLookupKey())
-                            .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(),
-                    null, null, null, null);
-            LongSparseArray<StreamItemEntry> streamItemsById =
-                    new LongSparseArray<StreamItemEntry>();
-            ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
-            try {
-                while (cursor.moveToNext()) {
-                    StreamItemEntry streamItem = new StreamItemEntry(cursor);
-                    streamItemsById.put(streamItem.getId(), streamItem);
-                    streamItems.add(streamItem);
-                }
-            } finally {
-                cursor.close();
-            }
-
-            // Pre-decode all HTMLs
-            final long start = System.currentTimeMillis();
-            for (StreamItemEntry streamItem : streamItems) {
-                streamItem.decodeHtml(getContext());
-            }
-            final long end = System.currentTimeMillis();
-            if (DEBUG) {
-                Log.d(TAG, "Decoded HTML for " + streamItems.size() + " items, took "
-                        + (end - start) + " ms");
-            }
-
-            // Now retrieve any photo records associated with the stream items.
-            if (!streamItems.isEmpty()) {
-                if (result.isUserProfile()) {
-                    // If the stream items we're loading are for the profile, we can't bulk-load the
-                    // stream items with a custom selection.
-                    for (StreamItemEntry entry : streamItems) {
-                        Cursor siCursor = getContext().getContentResolver().query(
-                                Uri.withAppendedPath(
-                                        ContentUris.withAppendedId(
-                                                StreamItems.CONTENT_URI, entry.getId()),
-                                        StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
-                                null, null, null, null);
-                        try {
-                            while (siCursor.moveToNext()) {
-                                entry.addPhoto(new StreamItemPhotoEntry(siCursor));
-                            }
-                        } finally {
-                            siCursor.close();
-                        }
-                    }
+                if (dataSet != null) {
+                    selection.append(" AND " + Groups.DATA_SET + "=?");
+                    selectionArgs.add(dataSet);
                 } else {
-                    String[] streamItemIdArr = new String[streamItems.size()];
-                    StringBuilder streamItemPhotoSelection = new StringBuilder();
-                    streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN (");
-                    for (int i = 0; i < streamItems.size(); i++) {
-                        if (i > 0) {
-                            streamItemPhotoSelection.append(",");
-                        }
-                        streamItemPhotoSelection.append("?");
-                        streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId());
-                    }
-                    streamItemPhotoSelection.append(")");
-                    Cursor sipCursor = getContext().getContentResolver().query(
-                            StreamItems.CONTENT_PHOTO_URI,
-                            null, streamItemPhotoSelection.toString(), streamItemIdArr,
-                            StreamItemPhotos.STREAM_ITEM_ID);
+                    selection.append(" AND " + Groups.DATA_SET + " IS NULL");
+                }
+                selection.append(")");
+            }
+        }
+        Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI,
+                GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]),
+                null);
+        try {
+            while (cursor.moveToNext()) {
+                final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+                final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+                final String dataSet = cursor.getString(GroupQuery.DATA_SET);
+                final long groupId = cursor.getLong(GroupQuery.ID);
+                final String title = cursor.getString(GroupQuery.TITLE);
+                final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD)
+                        ? false
+                        : cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+                final boolean favorites = cursor.isNull(GroupQuery.FAVORITES)
+                        ? false
+                        : cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+                result.addGroupMetaData(new GroupMetaData(
+                        accountName, accountType, dataSet, groupId, title, defaultGroup,
+                        favorites));
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Loads all stream items and stream item photos belonging to this contact.
+     */
+    private void loadStreamItems(Result result) {
+        Cursor cursor = getContext().getContentResolver().query(
+                Contacts.CONTENT_LOOKUP_URI.buildUpon()
+                        .appendPath(result.getLookupKey())
+                        .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(),
+                null, null, null, null);
+        LongSparseArray<StreamItemEntry> streamItemsById =
+                new LongSparseArray<StreamItemEntry>();
+        ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
+        try {
+            while (cursor.moveToNext()) {
+                StreamItemEntry streamItem = new StreamItemEntry(cursor);
+                streamItemsById.put(streamItem.getId(), streamItem);
+                streamItems.add(streamItem);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        // Pre-decode all HTMLs
+        final long start = System.currentTimeMillis();
+        for (StreamItemEntry streamItem : streamItems) {
+            streamItem.decodeHtml(getContext());
+        }
+        final long end = System.currentTimeMillis();
+        if (DEBUG) {
+            Log.d(TAG, "Decoded HTML for " + streamItems.size() + " items, took "
+                    + (end - start) + " ms");
+        }
+
+        // Now retrieve any photo records associated with the stream items.
+        if (!streamItems.isEmpty()) {
+            if (result.isUserProfile()) {
+                // If the stream items we're loading are for the profile, we can't bulk-load the
+                // stream items with a custom selection.
+                for (StreamItemEntry entry : streamItems) {
+                    Cursor siCursor = getContext().getContentResolver().query(
+                            Uri.withAppendedPath(
+                                    ContentUris.withAppendedId(
+                                            StreamItems.CONTENT_URI, entry.getId()),
+                                    StreamItems.StreamItemPhotos.CONTENT_DIRECTORY),
+                            null, null, null, null);
                     try {
-                        while (sipCursor.moveToNext()) {
-                            long streamItemId = sipCursor.getLong(
-                                    sipCursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID));
-                            StreamItemEntry streamItem = streamItemsById.get(streamItemId);
-                            streamItem.addPhoto(new StreamItemPhotoEntry(sipCursor));
+                        while (siCursor.moveToNext()) {
+                            entry.addPhoto(new StreamItemPhotoEntry(siCursor));
                         }
                     } finally {
-                        sipCursor.close();
+                        siCursor.close();
                     }
                 }
-            }
-
-            // Set the sorted stream items on the result.
-            Collections.sort(streamItems);
-            result.mStreamItems.addAll(streamItems);
-        }
-
-        @Override
-        protected void onPostExecute(Result result) {
-            unregisterObserver();
-
-            // The creator isn't interested in any further updates
-            if (mDestroyed || result == null) {
-                return;
-            }
-
-            mContact = result;
-
-            if (result.isLoaded()) {
-                mLookupUri = result.getLookupUri();
-
-                if (!result.isDirectoryEntry()) {
-                    Log.i(TAG, "Registering content observer for " + mLookupUri);
-                    if (mObserver == null) {
-                        mObserver = new ForceLoadContentObserver();
+            } else {
+                String[] streamItemIdArr = new String[streamItems.size()];
+                StringBuilder streamItemPhotoSelection = new StringBuilder();
+                streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN (");
+                for (int i = 0; i < streamItems.size(); i++) {
+                    if (i > 0) {
+                        streamItemPhotoSelection.append(",");
                     }
-                    getContext().getContentResolver().registerContentObserver(
-                            mLookupUri, true, mObserver);
+                    streamItemPhotoSelection.append("?");
+                    streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId());
                 }
+                streamItemPhotoSelection.append(")");
+                Cursor sipCursor = getContext().getContentResolver().query(
+                        StreamItems.CONTENT_PHOTO_URI,
+                        null, streamItemPhotoSelection.toString(), streamItemIdArr,
+                        StreamItemPhotos.STREAM_ITEM_ID);
+                try {
+                    while (sipCursor.moveToNext()) {
+                        long streamItemId = sipCursor.getLong(
+                                sipCursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID));
+                        StreamItemEntry streamItem = streamItemsById.get(streamItemId);
+                        streamItem.addPhoto(new StreamItemPhotoEntry(sipCursor));
+                    }
+                } finally {
+                    sipCursor.close();
+                }
+            }
+        }
 
-                // inform the source of the data that this contact is being looked at
-                postViewNotificationToSyncAdapter();
+        // Set the sorted stream items on the result.
+        Collections.sort(streamItems);
+        result.mStreamItems.addAll(streamItems);
+    }
+
+    @Override
+    public void deliverResult(Result result) {
+        unregisterObserver();
+
+        // The creator isn't interested in any further updates
+        if (isReset() || result == null) {
+            return;
+        }
+
+        mContact = result;
+
+        if (result.isLoaded()) {
+            mLookupUri = result.getLookupUri();
+
+            if (!result.isDirectoryEntry()) {
+                Log.i(TAG, "Registering content observer for " + mLookupUri);
+                if (mObserver == null) {
+                    mObserver = new ForceLoadContentObserver();
+                }
+                getContext().getContentResolver().registerContentObserver(
+                        mLookupUri, true, mObserver);
             }
 
-            deliverResult(mContact);
+            // inform the source of the data that this contact is being looked at
+            postViewNotificationToSyncAdapter();
         }
+
+        super.deliverResult(mContact);
     }
 
     /**
@@ -1205,20 +1214,6 @@
         }
     }
 
-    public ContactLoader(Context context, Uri lookupUri) {
-        this(context, lookupUri, false, false, false);
-    }
-
-    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
-            boolean loadStreamItems, boolean loadInvitableAccountTypes) {
-        super(context);
-        mLookupUri = lookupUri;
-        mRequestedUri = lookupUri;
-        mLoadGroupMetaData = loadGroupMetaData;
-        mLoadStreamItems = loadStreamItems;
-        mLoadInvitableAccountTypes = loadInvitableAccountTypes;
-    }
-
     /**
      * Sets whether to load stream items. Will trigger a reload if the value has changed.
      * At the moment, this is only used for debugging purposes
@@ -1250,15 +1245,15 @@
     }
 
     @Override
-    protected void onForceLoad() {
-        final LoadContactTask task = new LoadContactTask();
-        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
+    protected void onStopLoading() {
+        cancelLoad();
     }
 
     @Override
     protected void onReset() {
+        super.onReset();
+        cancelLoad();
         unregisterObserver();
         mContact = null;
-        mDestroyed = true;
     }
 }