Beginnings of a unit test for ContactsListActivity.

Change-Id: I4cfc83d79b13f7ffad31120534ee0287d59254aa
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 22cb78f..be94b7b 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -393,6 +393,7 @@
 
     int mMode = MODE_DEFAULT;
 
+    private boolean mRunQueriesSynchronously;
     private QueryHandler mQueryHandler;
     private boolean mJustCreated;
     private boolean mSyncEnabled;
@@ -589,6 +590,13 @@
         }
     };
 
+    /**
+     * Visible for testing: makes queries run on the UI thread.
+     */
+    /* package */ void runQueriesSynchronously() {
+        mRunQueriesSynchronously = true;
+    }
+
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -1064,48 +1072,49 @@
 
         // This query can be performed on the UI thread because
         // the API explicitly allows such use.
-        Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI, new String[] {
-                ProviderStatus.STATUS, ProviderStatus.DATA1
-        }, null, null, null);
-        try {
-            if (cursor.moveToFirst()) {
-                int status = cursor.getInt(0);
-                if (status != mProviderStatus) {
-                    mProviderStatus = status;
-                    switch (status) {
-                        case ProviderStatus.STATUS_NORMAL:
-                            mAdapter.notifyDataSetInvalidated();
-                            if (loadData) {
-                                startQuery();
-                            }
-                            break;
+        Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI,
+                new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    int status = cursor.getInt(0);
+                    if (status != mProviderStatus) {
+                        mProviderStatus = status;
+                        switch (status) {
+                            case ProviderStatus.STATUS_NORMAL:
+                                mAdapter.notifyDataSetInvalidated();
+                                if (loadData) {
+                                    startQuery();
+                                }
+                                break;
 
-                        case ProviderStatus.STATUS_CHANGING_LOCALE:
-                            messageView.setText(R.string.locale_change_in_progress);
-                            mAdapter.changeCursor(null);
-                            mAdapter.notifyDataSetInvalidated();
-                            break;
+                            case ProviderStatus.STATUS_CHANGING_LOCALE:
+                                messageView.setText(R.string.locale_change_in_progress);
+                                mAdapter.changeCursor(null);
+                                mAdapter.notifyDataSetInvalidated();
+                                break;
 
-                        case ProviderStatus.STATUS_UPGRADING:
-                            messageView.setText(R.string.upgrade_in_progress);
-                            mAdapter.changeCursor(null);
-                            mAdapter.notifyDataSetInvalidated();
-                            break;
+                            case ProviderStatus.STATUS_UPGRADING:
+                                messageView.setText(R.string.upgrade_in_progress);
+                                mAdapter.changeCursor(null);
+                                mAdapter.notifyDataSetInvalidated();
+                                break;
 
-                        case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
-                            long size = cursor.getLong(1);
-                            String message = getResources().getString(
-                                    R.string.upgrade_out_of_memory, new Object[] {size});
-                            messageView.setText(message);
-                            configureImportFailureView(importFailureView);
-                            mAdapter.changeCursor(null);
-                            mAdapter.notifyDataSetInvalidated();
-                            break;
+                            case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
+                                long size = cursor.getLong(1);
+                                String message = getResources().getString(
+                                        R.string.upgrade_out_of_memory, new Object[] {size});
+                                messageView.setText(message);
+                                configureImportFailureView(importFailureView);
+                                mAdapter.changeCursor(null);
+                                mAdapter.notifyDataSetInvalidated();
+                                break;
+                        }
                     }
                 }
+            } finally {
+                cursor.close();
             }
-        } finally {
-            cursor.close();
         }
 
         importFailureView.setVisibility(
@@ -2904,6 +2913,19 @@
         }
 
         @Override
+        public void startQuery(int token, Object cookie, Uri uri, String[] projection,
+                String selection, String[] selectionArgs, String orderBy) {
+            final ContactsListActivity activity = mActivity.get();
+            if (activity != null && activity.mRunQueriesSynchronously) {
+                Cursor cursor = getContentResolver().query(uri, projection, selection,
+                        selectionArgs, orderBy);
+                onQueryComplete(token, cookie, cursor);
+            } else {
+                super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
+            }
+        }
+
+        @Override
         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
             final ContactsListActivity activity = mActivity.get();
             if (activity != null && !activity.isFinishing()) {
diff --git a/tests/src/com/android/contacts/ContactListModeTest.java b/tests/src/com/android/contacts/ContactListModeTest.java
new file mode 100644
index 0000000..fe8ef5c
--- /dev/null
+++ b/tests/src/com/android/contacts/ContactListModeTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2009 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.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockContentProvider;
+
+import android.content.Intent;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.provider.ContactsContract.ContactCounts;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.ProviderStatus;
+import android.provider.ContactsContract.StatusUpdates;
+import android.test.ActivityUnitTestCase;
+
+/**
+ * Tests for the contact list activity modes.
+ *
+ * Running all tests:
+ *
+ *   runtest contacts
+ * or
+ *   adb shell am instrument \
+ *     -w com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+public class ContactListModeTest
+        extends ActivityUnitTestCase<ContactsListActivity> {
+
+    private ContactsMockContext mContext;
+    private MockContentProvider mContactsProvider;
+    private MockContentProvider mSettingsProvider;
+
+    public ContactListModeTest() {
+        super(ContactsListActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = new ContactsMockContext(getInstrumentation().getTargetContext());
+        mContactsProvider = mContext.getContactsProvider();
+        mSettingsProvider = mContext.getSettingsProvider();
+        setActivityContext(mContext);
+    }
+
+    public void testDefaultMode() throws Exception {
+        mContactsProvider.expectQuery(ProviderStatus.CONTENT_URI)
+                .withProjection(ProviderStatus.STATUS, ProviderStatus.DATA1);
+
+        mSettingsProvider.expectQuery(Settings.System.CONTENT_URI)
+                .withProjection(Settings.System.VALUE)
+                .withSelection(Settings.System.NAME + "=?",
+                        ContactsContract.Preferences.SORT_ORDER);
+
+        mSettingsProvider.expectQuery(Settings.System.CONTENT_URI)
+                .withProjection(Settings.System.VALUE)
+                .withSelection(Settings.System.NAME + "=?",
+                        ContactsContract.Preferences.DISPLAY_ORDER);
+
+        mContactsProvider.expectQuery(
+                Contacts.CONTENT_URI.buildUpon()
+                        .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true")
+                        .build())
+                .withProjection(
+                        Contacts._ID,
+                        Contacts.DISPLAY_NAME,
+                        Contacts.DISPLAY_NAME_ALTERNATIVE,
+                        Contacts.SORT_KEY_PRIMARY,
+                        Contacts.STARRED,
+                        Contacts.TIMES_CONTACTED,
+                        Contacts.CONTACT_PRESENCE,
+                        Contacts.PHOTO_ID,
+                        Contacts.LOOKUP_KEY,
+                        Contacts.PHONETIC_NAME,
+                        Contacts.HAS_PHONE_NUMBER)
+                .withSelection(Contacts.IN_VISIBLE_GROUP + "=1")
+                .withSortOrder(Contacts.SORT_KEY_PRIMARY)
+                .returnRow(1, "John", "John", "john", 1, 10,
+                        StatusUpdates.AVAILABLE, 23, "lk1", "john", 1)
+                .returnRow(2, "Jim", "Jim", "jim", 1, 8,
+                        StatusUpdates.AWAY, 24, "lk2", "jim", 0);
+
+        Intent intent = new Intent(Intent.ACTION_VIEW, ContactsContract.Contacts.CONTENT_URI);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent, null, null);
+        ContactsListActivity activity = getActivity();
+        activity.runQueriesSynchronously();
+        activity.onResume();        // Trigger the queries
+
+        assertEquals(3, activity.getListAdapter().getCount());
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
new file mode 100644
index 0000000..bd2010e
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -0,0 +1,58 @@
+/*
+ * 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.tests.mocks;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+
+/**
+ * A mock context for contact activity unit tests. Forwards everything to
+ * a supplied context, except content resolver operations, which are sent
+ * to mock content providers.
+ */
+public class ContactsMockContext extends ContextWrapper {
+
+    private MockContentResolver mContentResolver;
+    private MockContentProvider mContactsProvider;
+    private MockContentProvider mSettingsProvider;
+
+    public ContactsMockContext(Context base) {
+        super(base);
+        mContentResolver = new MockContentResolver();
+        mContactsProvider = new MockContentProvider();
+        mContentResolver.addProvider(ContactsContract.AUTHORITY, mContactsProvider);
+        mSettingsProvider = new MockContentProvider();
+        mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
+    }
+
+    @Override
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    public MockContentProvider getContactsProvider() {
+        return mContactsProvider;
+    }
+
+    public MockContentProvider getSettingsProvider() {
+        return mSettingsProvider;
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java
new file mode 100644
index 0000000..63b134a
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/mocks/MockContentProvider.java
@@ -0,0 +1,207 @@
+/*
+ * 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.tests.mocks;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedList;
+
+import junit.framework.Assert;
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockContentProvider extends ContentProvider {
+
+    public static class Query {
+
+        private final Uri mUri;
+        private String[] mProjection;
+        private String[] mDefaultProjection;
+        private String mSelection;
+        private String[] mSelectionArgs;
+        private String mSortOrder;
+        private ArrayList<Object[]> mRows = new ArrayList<Object[]>();
+
+        public Query(Uri uri) {
+            mUri = uri;
+        }
+
+        @Override
+        public String toString() {
+            return queryToString(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder);
+        }
+
+        public Query withProjection(String... projection) {
+            mProjection = projection;
+            return this;
+        }
+
+        public Query withDefaultProjection(String... projection) {
+            mDefaultProjection = projection;
+            return this;
+        }
+
+        public Query withSelection(String selection, String... selectionArgs) {
+            mSelection = selection;
+            mSelectionArgs = selectionArgs;
+            return this;
+        }
+
+        public Query withSortOrder(String sortOrder) {
+            mSortOrder = sortOrder;
+            return this;
+        }
+
+        public Query returnRow(Object... row) {
+            mRows.add(row);
+            return this;
+        }
+
+        public boolean equals(Uri uri, String[] projection, String selection,
+                String[] selectionArgs, String sortOrder) {
+            if (!uri.equals(mUri)) {
+                return false;
+            }
+
+            if (!equals(projection, mProjection)) {
+                return false;
+            }
+
+            if (!TextUtils.equals(selection, mSelection)) {
+                return false;
+            }
+
+            if (!equals(selectionArgs, mSelectionArgs)) {
+                return false;
+            }
+
+            if (!TextUtils.equals(sortOrder, mSortOrder)) {
+                return false;
+            }
+
+            return true;
+        }
+
+        private boolean equals(String[] array1, String[] array2) {
+            boolean empty1 = array1 == null || array1.length == 0;
+            boolean empty2 = array2 == null || array2.length == 0;
+            if (empty1 && empty2) {
+                return true;
+            }
+            if (empty1) {
+                return false;
+            }
+
+            for (int i = 0; i < array1.length; i++) {
+                if (!array1[i].equals(array2[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        public Cursor getResult() {
+            String[] columnNames = mProjection != null ? mProjection : mDefaultProjection;
+            MatrixCursor cursor = new MatrixCursor(columnNames);
+            for (Object[] row : mRows) {
+                cursor.addRow(row);
+            }
+            return cursor;
+        }
+    }
+
+    private LinkedList<Query> mExpectedQueries = new LinkedList<Query>();
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    public Query expectQuery(Uri contentUri) {
+        Query query = new Query(contentUri);
+        mExpectedQueries.offer(query);
+        return query;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        if (mExpectedQueries.isEmpty()) {
+            Assert.fail("Unexpected query: "
+                    + queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        }
+
+        Query query = mExpectedQueries.remove();
+        if (!query.equals(uri, projection, selection, selectionArgs, sortOrder)) {
+            Assert.fail("Incorrect query.\n    Expected: " + query + "\n      Actual: " +
+                    queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        }
+
+        return query.getResult();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    private static String queryToString(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(uri).append(" ");
+        if (projection != null) {
+            sb.append(Arrays.toString(projection));
+        } else {
+            sb.append("[]");
+        }
+        if (selection != null) {
+            sb.append(" selection: '").append(selection).append("'");
+            if (projection != null) {
+                sb.append(Arrays.toString(selectionArgs));
+            } else {
+                sb.append("[]");
+            }
+        }
+        if (sortOrder != null) {
+            sb.append(" sort: '").append(sortOrder).append("'");
+        }
+        return sb.toString();
+    }
+}