Write tests for ContactLoader. Re-added support for legacy style (donut) ContactUris

Change-Id: If20497bff840055c923a91b9cfcd61d22454d49e
diff --git a/src/com/android/contacts/views/detail/ContactLoader.java b/src/com/android/contacts/views/detail/ContactLoader.java
index 4528387..103136a 100644
--- a/src/com/android/contacts/views/detail/ContactLoader.java
+++ b/src/com/android/contacts/views/detail/ContactLoader.java
@@ -27,6 +27,7 @@
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.DisplayNameSources;
@@ -137,15 +138,25 @@
         final int _ID = 0;
     }
 
-    final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+    public final class LoadContactTask extends AsyncTask<Void, Void, Result> {
+
+        /**
+         * Used for synchronuous calls in unit test
+         * @hide
+         */
+        public Result testExecute() {
+            return doInBackground();
+        }
+
         @Override
         protected Result doInBackground(Void... args) {
             final ContentResolver resolver = getContext().getContentResolver();
-            Result result = loadContactHeaderData(resolver, mLookupUri);
+            Uri uriCurrentFormat = convertLegacyIfNecessary(mLookupUri);
+            Result result = loadContactHeaderData(resolver, uriCurrentFormat);
             if (result == Result.NOT_FOUND) {
                 // No record found. Try to lookup up a new record with the same lookupKey.
                 // We might have went through a sync where Ids changed
-                final Uri freshLookupUri = Contacts.getLookupUri(resolver, mLookupUri);
+                final Uri freshLookupUri = Contacts.getLookupUri(resolver, uriCurrentFormat);
                 result = loadContactHeaderData(resolver, freshLookupUri);
                 if (result == Result.NOT_FOUND) {
                     // Still not found. We now believe this contact really does not exist
@@ -163,6 +174,32 @@
         }
 
         /**
+         * Transforms the given Uri and returns a Lookup-Uri that represents the contact.
+         * For legacy contacts, a raw-contact lookup is performed.
+         */
+        private Uri convertLegacyIfNecessary(Uri uri) {
+            if (uri == null) throw new IllegalArgumentException("uri must not be null");
+
+            final String authority = uri.getAuthority();
+
+            // Current Style Uri? Just return it
+            if (ContactsContract.AUTHORITY.equals(authority)) {
+                return uri;
+            }
+
+            // Legacy Style? Convert to RawContact
+            final String OBSOLETE_AUTHORITY = "contacts";
+            if (OBSOLETE_AUTHORITY.equals(authority)) {
+                // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+                final long rawContactId = ContentUris.parseId(uri);
+                return RawContacts.getContactLookupUri(getContext().getContentResolver(),
+                        ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+            }
+
+            throw new IllegalArgumentException("uri format is unknown");
+        }
+
+        /**
          * Tries to lookup a contact using both Id and lookup key of the given Uri. Returns a
          * valid Result instance if successful or {@link Result#NOT_FOUND} if empty
          */
@@ -178,7 +215,7 @@
             if (segments.size() != 4) {
                 // Does not contain an Id. Return to caller so that a lookup is performed
                 Log.w(TAG, "Uri does not contain an Id, so we return to the caller who should " +
-                		"perform a lookup to get a proper uri. Value: " + lookupUri);
+                        "perform a lookup to get a proper uri. Value: " + lookupUri);
                 return Result.NOT_FOUND;
             }
 
@@ -200,7 +237,7 @@
             try {
                 if (!cursor.moveToFirst()) {
                     Log.w(TAG, "Cursor returned by trySetupContactHeader/query is empty. " +
-                    		"ContactId must have changed or item has been removed");
+                            "ContactId must have changed or item has been removed");
                     return Result.NOT_FOUND;
                 }
                 String lookupKey =
@@ -300,7 +337,7 @@
 
         @Override
         protected void onPostExecute(Result result) {
-            // The creator isn't interested in any furether updates
+            // The creator isn't interested in any further updates
             if (mDestroyed) {
                 return;
             }
@@ -319,7 +356,7 @@
     }
 
     public ContactLoader(Context context, Uri lookupUri) {
-        super(context);
+            super(context);
         mLookupUri = lookupUri;
     }
 
diff --git a/tests/src/com/android/contacts/ContactDetailLoaderTest.java b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
new file mode 100644
index 0000000..f7a7a84
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactDetailLoaderTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.contacts.views.detail.ContactLoader;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.test.AndroidTestCase;
+
+import junit.framework.AssertionFailedError;
+
+/**
+ * Runs ContactLoader tests for the the contact-detail view.
+ * TODO: Warning: This currently only works on wiped phones as this will wipe
+ * your contact data
+ * TODO: Test all fields returned by the Loader
+ * TODO: Test social entries returned by the Loader
+ */
+public class ContactDetailLoaderTest extends AndroidTestCase {
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        //mContext.getContentResolver().delete(Data.CONTENT_URI, null, null);
+        //mContext.getContentResolver().delete(RawContacts.CONTENT_URI, null, null);
+    }
+
+    /**
+     * Utility function to ensure that an Exception is thrown during the code
+     * TODO: This should go to MoreAsserts at one point
+     */
+    @SuppressWarnings("unchecked")
+    private static <E extends Throwable> E assertThrows(
+            Class<E> expectedException, Runnable runnable) {
+        try {
+            runnable.run();
+        } catch (Throwable exception) {
+            Class<? extends Throwable> receivedException = exception.getClass();
+            if (expectedException == receivedException) return (E) exception;
+            throw new AssertionFailedError("Expected Exception " + expectedException +
+                    " but " + receivedException + " was thrown. Details: " + exception);
+        }
+        throw new AssertionFailedError(
+                "Expected Exception " + expectedException + " which was not thrown");
+    }
+
+    private ContactLoader.Result assertLoadContact(Uri uri) {
+        final ContactLoader loader = new ContactLoader(mContext, uri);
+        final ContactLoader.LoadContactTask loadContactTask = loader.new LoadContactTask();
+        return loadContactTask.testExecute();
+    }
+
+    protected Uri insertStructuredName(long rawContactId, String givenName, String familyName) {
+        ContentValues values = new ContentValues();
+        StringBuilder sb = new StringBuilder();
+        if (givenName != null) {
+            sb.append(givenName);
+        }
+        if (givenName != null && familyName != null) {
+            sb.append(" ");
+        }
+        if (familyName != null) {
+            sb.append(familyName);
+        }
+        values.put(StructuredName.DISPLAY_NAME, sb.toString());
+        values.put(StructuredName.GIVEN_NAME, givenName);
+        values.put(StructuredName.FAMILY_NAME, familyName);
+
+        return insertStructuredName(rawContactId, values);
+    }
+
+    protected Uri insertStructuredName(long rawContactId, ContentValues values) {
+        values.put(Data.RAW_CONTACT_ID, rawContactId);
+        values.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        Uri resultUri = getContext().getContentResolver().insert(Data.CONTENT_URI, values);
+        return resultUri;
+    }
+
+    protected Cursor queryRawContact(long rawContactId) {
+        return getContext().getContentResolver().query(ContentUris.withAppendedId(
+                RawContacts.CONTENT_URI, rawContactId), null, null, null, null);
+    }
+
+    protected Cursor queryContact(long contactId) {
+        return getContext().getContentResolver().query(ContentUris.withAppendedId(
+                Contacts.CONTENT_URI, contactId), null, null, null, null);
+    }
+
+    private long getContactIdByRawContactId(long rawContactId) {
+        Cursor c = queryRawContact(rawContactId);
+        assertTrue(c.moveToFirst());
+        long contactId = c.getLong(c.getColumnIndex(RawContacts.CONTACT_ID));
+        c.close();
+        return contactId;
+    }
+
+    private String getContactLookupByContactId(long contactId) {
+        Cursor c = queryContact(contactId);
+        assertTrue(c.moveToFirst());
+        String lookup = c.getString(c.getColumnIndex(Contacts.LOOKUP_KEY));
+        c.close();
+        return lookup;
+    }
+
+    public long createRawContact(String sourceId, String givenName, String familyName) {
+        ContentValues values = new ContentValues();
+
+        values.put(RawContacts.ACCOUNT_NAME, "aa");
+        values.put(RawContacts.ACCOUNT_TYPE, "mock");
+        values.put(RawContacts.SOURCE_ID, sourceId);
+        values.put(RawContacts.VERSION, 1);
+        values.put(RawContacts.DELETED, 0);
+        values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
+        values.put(RawContacts.CUSTOM_RINGTONE, "d");
+        values.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+        values.put(RawContacts.LAST_TIME_CONTACTED, 12345);
+        values.put(RawContacts.STARRED, 1);
+        values.put(RawContacts.SYNC1, "e");
+        values.put(RawContacts.SYNC2, "f");
+        values.put(RawContacts.SYNC3, "g");
+        values.put(RawContacts.SYNC4, "h");
+
+        Uri rawContactUri =
+            getContext().getContentResolver().insert(RawContacts.CONTENT_URI, values);
+
+        long rawContactId = ContentUris.parseId(rawContactUri);
+        insertStructuredName(rawContactId, givenName, familyName);
+        return rawContactId;
+    }
+
+    public void testNullUri() {
+        IllegalArgumentException e =
+            assertThrows(IllegalArgumentException.class, new Runnable() {
+                public void run() {
+                    assertLoadContact(null);
+                }
+            });
+        assertEquals(e.getMessage(), "uri must not be null");
+    }
+
+    public void testEmptyUri() {
+        IllegalArgumentException e =
+            assertThrows(IllegalArgumentException.class, new Runnable() {
+                public void run() {
+                    assertLoadContact(Uri.EMPTY);
+                }
+            });
+        assertEquals(e.getMessage(), "uri format is unknown");
+    }
+
+    public void testInvalidUri() {
+        IllegalArgumentException e =
+                assertThrows(IllegalArgumentException.class, new Runnable() {
+                    public void run() {
+                        assertLoadContact(Uri.parse("content://wtf"));
+                    }
+                });
+        assertEquals(e.getMessage(), "uri format is unknown");
+    }
+
+    public void testLoadContactWithContactIdUri() {
+        // Use content Uris that only contain the ID
+        // Use some special characters in the source id to ensure that Encode/Decode properly
+        // works in Uris
+        long rawContactId1 = createRawContact("JohnDoe:;\"'[]{}=+-_\\|/.,<>?!@#$", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh%12%%^&*()", "Jane", "Duh");
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        Uri contactUri1 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId1);
+        Uri contactUri2 = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId2);
+
+        ContactLoader.Result contact1 = assertLoadContact(contactUri1);
+        ContactLoader.Result contact2 = assertLoadContact(contactUri2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithOldStyleUri() {
+        // Use content Uris that only contain the ID but use the format used in Donut
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        Uri oldUri1 = ContentUris.withAppendedId(Uri.parse("content://contacts"), rawContactId1);
+        Uri oldUri2 = ContentUris.withAppendedId(Uri.parse("content://contacts"), rawContactId2);
+
+        ContactLoader.Result contact1 = assertLoadContact(oldUri1);
+        ContactLoader.Result contact2 = assertLoadContact(oldUri2);
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithContactLookupUri() {
+        // Use lookup-style Uris that do not contain the Contact-ID
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        assertTrue(rawContactId1 != rawContactId2);
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        assertTrue(contactId1 != contactId2);
+
+        String lookupKey1 = getContactLookupByContactId(contactId1);
+        String lookupKey2 = getContactLookupByContactId(contactId2);
+        assertFalse(lookupKey1.equals(lookupKey2));
+
+        Uri contactLookupUri1 = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1);
+        Uri contactLookupUri2 = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2);
+
+        ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+        ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithContactLookupAndIdUri() {
+        // Use lookup-style Uris that also contain the Contact-ID
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        String lookupKey1 = getContactLookupByContactId(contactId1);
+        String lookupKey2 = getContactLookupByContactId(contactId2);
+
+        Uri contactLookupUri1 = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1), contactId1);
+        Uri contactLookupUri2 = ContentUris.withAppendedId(
+                Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2), contactId2);
+
+        ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+        ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+        assertEquals(contactId1, contact1.getId());
+        assertEquals(contactId2, contact2.getId());
+    }
+
+    public void testLoadContactWithContactLookupWithIncorrectIdUri() {
+        // Use lookup-style Uris that contain incorrect Contact-ID
+        // (we want to ensure that still the correct contact is chosen)
+
+        long rawContactId1 = createRawContact("JohnDoe", "John", "Doe");
+        long rawContactId2 = createRawContact("JaneDuh", "Jane", "Duh");
+
+        long contactId1 = getContactIdByRawContactId(rawContactId1);
+        long contactId2 = getContactIdByRawContactId(rawContactId2);
+
+        String lookupKey1 = getContactLookupByContactId(contactId1);
+        String lookupKey2 = getContactLookupByContactId(contactId2);
+
+        long[] fakeIds = new long[] { 0, rawContactId1, rawContactId2, contactId1, contactId2 };
+
+        for (long fakeContactId : fakeIds) {
+            Uri contactLookupUri1 = ContentUris.withAppendedId(
+                    Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey1), fakeContactId);
+            Uri contactLookupUri2 = ContentUris.withAppendedId(
+                    Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey2), fakeContactId);
+
+            ContactLoader.Result contact1 = assertLoadContact(contactLookupUri1);
+            ContactLoader.Result contact2 = assertLoadContact(contactLookupUri2);
+
+            assertEquals(contactId1, contact1.getId());
+            assertEquals(contactId2, contact2.getId());
+        }
+    }
+}