Making a LoaderManagingFragment testable

Change-Id: Ie7da83a96dd4be34637efcc3c885e2889fede2ff
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
index 9e208bc..c4ed0d7 100644
--- a/src/com/android/contacts/list/ContactEntryListFragment.java
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -23,12 +23,12 @@
 import com.android.contacts.R;
 import com.android.contacts.ui.ContactsPreferences;
 import com.android.contacts.widget.ContextMenuAdapter;
+import com.android.contacts.widget.InstrumentedLoaderManagingFragment;
 import com.android.contacts.widget.CompositeCursorAdapter.Partition;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.app.Activity;
-import android.app.LoaderManagingFragment;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -70,7 +70,7 @@
  * Common base class for various contact-related list fragments.
  */
 public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter>
-        extends LoaderManagingFragment<Cursor>
+        extends InstrumentedLoaderManagingFragment<Cursor>
         implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener {
 
     public static final int ACTIVITY_REQUEST_CODE_PICKER = 1;
@@ -124,6 +124,8 @@
 
     private ContactsRequest mRequest;
 
+    private Context mContext;
+
     protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
     protected abstract T createListAdapter();
 
@@ -134,6 +136,24 @@
      */
     protected abstract void onItemClick(int position, long id);
 
+    @Override
+    public void onAttach(Activity activity) {
+        super.onAttach(activity);
+        setContext(activity);
+    }
+
+    /**
+     * Sets a context for the fragment in the unit test environment.
+     */
+    public void setContext(Context context) {
+        mContext = context;
+        configurePhotoLoader();
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
     public T getAdapter() {
         return mAdapter;
     }
@@ -172,11 +192,11 @@
     @Override
     public void onStart() {
         if (mContactsPrefs == null) {
-            mContactsPrefs = new ContactsPreferences(getActivity());
+            mContactsPrefs = new ContactsPreferences(mContext);
         }
 
         if (mProviderStatusLoader == null) {
-            mProviderStatusLoader = new ProviderStatusLoader(getActivity());
+            mProviderStatusLoader = new ProviderStatusLoader(mContext);
         }
 
         loadPreferences(mContactsPrefs);
@@ -212,11 +232,11 @@
     @Override
     protected Loader<Cursor> onCreateLoader(int id, Bundle args) {
         if (id == DIRECTORY_LOADER_ID) {
-            DirectoryListLoader loader = new DirectoryListLoader(getActivity());
+            DirectoryListLoader loader = new DirectoryListLoader(mContext);
             mAdapter.configureDirectoryLoader(loader);
             return loader;
         } else {
-            CursorLoader loader = new CursorLoader(getActivity(), null, null, null, null, null);
+            CursorLoader loader = new CursorLoader(mContext, null, null, null, null, null);
             long directoryId = args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
                     ? args.getLong(DIRECTORY_ID_ARG_KEY)
                     : Directory.DEFAULT;
@@ -460,12 +480,6 @@
         return mContextMenuAdapter;
     }
 
-    @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        configurePhotoLoader();
-    }
-
     protected void loadPreferences(ContactsPreferences contactsPrefs) {
         setContactNameDisplayOrder(contactsPrefs.getDisplayOrder());
         setSortOrder(contactsPrefs.getSortOrder());
@@ -540,10 +554,9 @@
     }
 
     protected void configurePhotoLoader() {
-        Activity activity = getActivity();
-        if (isPhotoLoaderEnabled() && activity != null) {
+        if (isPhotoLoaderEnabled() && mContext != null) {
             if (mPhotoLoader == null) {
-                mPhotoLoader = new ContactPhotoLoader(activity, R.drawable.ic_contact_list_picture);
+                mPhotoLoader = new ContactPhotoLoader(mContext, R.drawable.ic_contact_list_picture);
             }
             if (mListView != null) {
                 mListView.setOnScrollListener(this);
@@ -558,7 +571,7 @@
         if (isSearchResultsMode() && mView != null) {
             TextView titleText = (TextView)mView.findViewById(R.id.search_results_for);
             if (titleText != null) {
-                titleText.setText(Html.fromHtml(getActivity().getString(R.string.search_results_for,
+                titleText.setText(Html.fromHtml(mContext.getString(R.string.search_results_for,
                         "<b>" + getQueryString() + "</b>")));
             }
         }
@@ -638,7 +651,7 @@
     private void hideSoftKeyboard() {
         // Hide soft keyboard, if visible
         InputMethodManager inputMethodManager = (InputMethodManager)
-                getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+                mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
         inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
     }
 
@@ -706,7 +719,7 @@
      * reflect them in the UI.
      */
     private void registerProviderStatusObserver() {
-        getActivity().getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI,
+        mContext.getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI,
                 false, mProviderStatusObserver);
     }
 
@@ -715,7 +728,7 @@
      * reflect them in the UI.
      */
     private void unregisterProviderStatusObserver() {
-        getActivity().getContentResolver().unregisterContentObserver(mProviderStatusObserver);
+        mContext.getContentResolver().unregisterContentObserver(mProviderStatusObserver);
     }
 
     /**
@@ -733,7 +746,7 @@
 
         // This query can be performed on the UI thread because
         // the API explicitly allows such use.
-        Cursor cursor = getActivity().getContentResolver().query(ProviderStatus.CONTENT_URI,
+        Cursor cursor = mContext.getContentResolver().query(ProviderStatus.CONTENT_URI,
                 new String[] { ProviderStatus.STATUS, ProviderStatus.DATA1 }, null, null, null);
         if (cursor != null) {
             try {
@@ -763,7 +776,7 @@
 
                             case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
                                 long size = cursor.getLong(1);
-                                String message = getActivity().getResources().getString(
+                                String message = mContext.getResources().getString(
                                         R.string.upgrade_out_of_memory, new Object[] {size});
                                 TextView messageView = (TextView) findViewById(R.id.emptyText);
                                 messageView.setText(message);
@@ -795,15 +808,14 @@
                 switch(v.getId()) {
                     case R.id.import_failure_uninstall_apps: {
                         // TODO break into a separate method
-                        getActivity().startActivity(
-                                new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
+                        startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS));
                         break;
                     }
                     case R.id.import_failure_retry_upgrade: {
                         // Send a provider status update, which will trigger a retry
                         ContentValues values = new ContentValues();
                         values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
-                        getActivity().getContentResolver().update(ProviderStatus.CONTENT_URI,
+                        mContext.getContentResolver().update(ProviderStatus.CONTENT_URI,
                                 values, null, null);
                         break;
                     }
@@ -824,9 +836,9 @@
     // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
     public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
         if (count == 0) {
-            return getActivity().getString(zeroResourceId);
+            return mContext.getString(zeroResourceId);
         } else {
-            String format = getActivity().getResources()
+            String format = mContext.getResources()
                     .getQuantityText(pluralResourceId, count).toString();
             return String.format(format, count);
         }
@@ -834,13 +846,13 @@
 
     protected void setEmptyText(int resourceId) {
         TextView empty = (TextView) getEmptyView().findViewById(R.id.emptyText);
-        empty.setText(getActivity().getText(resourceId));
+        empty.setText(mContext.getText(resourceId));
         empty.setVisibility(View.VISIBLE);
     }
 
     // TODO redesign into an async task or loader
     protected boolean isSyncActive() {
-        Account[] accounts = AccountManager.get(getActivity()).getAccounts();
+        Account[] accounts = AccountManager.get(mContext).getAccounts();
         if (accounts != null && accounts.length > 0) {
             IContentService contentService = ContentResolver.getContentService();
             for (Account account : accounts) {
@@ -858,7 +870,7 @@
 
     protected boolean hasIccCard() {
         TelephonyManager telephonyManager =
-                (TelephonyManager)getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+                (TelephonyManager)mContext.getSystemService(Context.TELEPHONY_SERVICE);
         return telephonyManager.hasIccCard();
     }
 
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index dfc4c26..9289404 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -62,7 +62,7 @@
                 return true;
             case ContactsRequest.DISPLAY_ONLY_WITH_PHONES_PREFERENCE:
                 SharedPreferences prefs = PreferenceManager
-                        .getDefaultSharedPreferences(getActivity());
+                        .getDefaultSharedPreferences(getContext());
                 return prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
                         Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
         }
@@ -95,7 +95,7 @@
 
     @Override
     protected ContactListAdapter createListAdapter() {
-        DefaultContactListAdapter adapter = new DefaultContactListAdapter(getActivity());
+        DefaultContactListAdapter adapter = new DefaultContactListAdapter(getContext());
         adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled());
         adapter.setDisplayPhotos(true);
         adapter.setQuickContactEnabled(true);
diff --git a/src/com/android/contacts/widget/InstrumentedLoaderManagingFragment.java b/src/com/android/contacts/widget/InstrumentedLoaderManagingFragment.java
new file mode 100644
index 0000000..e146501
--- /dev/null
+++ b/src/com/android/contacts/widget/InstrumentedLoaderManagingFragment.java
@@ -0,0 +1,49 @@
+/*
+ * 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.widget;
+
+import android.app.LoaderManagingFragment;
+import android.content.Loader;
+import android.os.Bundle;
+
+/**
+ * A modification of the {@link LoaderManagingFragment} class that supports testing of
+ * loader-based fragments using synchronous data loading.
+ */
+public abstract class InstrumentedLoaderManagingFragment<D> extends LoaderManagingFragment<D> {
+
+    public interface Delegate<D> {
+        void onStartLoading(Loader<D> loader);
+    }
+
+    private Delegate<D> mDelegate;
+
+    public void setDelegate(Delegate<D> listener) {
+        this.mDelegate = listener;
+    }
+
+    @Override
+    protected Loader<D> startLoading(int id, Bundle args) {
+        if (mDelegate != null) {
+            Loader<D> loader = onCreateLoader(id, args);
+            loader.registerListener(id, this);
+            mDelegate.onStartLoading(loader);
+            return loader;
+        } else {
+            return super.startLoading(id, args);
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/ContactListModeTest.java b/tests/src/com/android/contacts/DefaultContactBrowseListFragmentTest.java
similarity index 65%
rename from tests/src/com/android/contacts/ContactListModeTest.java
rename to tests/src/com/android/contacts/DefaultContactBrowseListFragmentTest.java
index f09cc8d..cfc87d9 100644
--- a/tests/src/com/android/contacts/ContactListModeTest.java
+++ b/tests/src/com/android/contacts/DefaultContactBrowseListFragmentTest.java
@@ -16,22 +16,28 @@
 
 package com.android.contacts;
 
+import com.android.contacts.list.DefaultContactBrowseListFragment;
 import com.android.contacts.tests.mocks.ContactsMockContext;
 import com.android.contacts.tests.mocks.MockContentProvider;
+import com.android.contacts.widget.LoaderManagingFragmentTestDelegate;
 
-import android.content.Intent;
+import android.database.Cursor;
 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;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.Smoke;
+import android.view.LayoutInflater;
+import android.view.View;
 import android.widget.ListAdapter;
 import android.widget.ListView;
 
+
 /**
- * Tests for the contact list activity modes.
+ * Tests for {@link DefaultContactBrowseListFragment}.
  *
  * Running all tests:
  *
@@ -40,40 +46,34 @@
  *   adb shell am instrument \
  *     -w com.android.contacts.tests/android.test.InstrumentationTestRunner
  */
-public class ContactListModeTest
-        extends ActivityUnitTestCase<ContactsListActivity> {
+@Smoke
+public class DefaultContactBrowseListFragmentTest
+        extends InstrumentationTestCase {
 
     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);
 
+        mSettingsProvider.expectQuery(Settings.System.CONTENT_URI)
+                .withProjection(Settings.System.VALUE)
+                .withSelection(Settings.System.NAME + "=?",
+                        ContactsContract.Preferences.SORT_ORDER);
+
         mContactsProvider.expectQuery(
                 Contacts.CONTENT_URI.buildUpon()
                         .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true")
@@ -84,7 +84,6 @@
                         Contacts.DISPLAY_NAME_ALTERNATIVE,
                         Contacts.SORT_KEY_PRIMARY,
                         Contacts.STARRED,
-                        Contacts.TIMES_CONTACTED,
                         Contacts.CONTACT_PRESENCE,
                         Contacts.PHOTO_ID,
                         Contacts.LOOKUP_KEY,
@@ -92,19 +91,45 @@
                         Contacts.HAS_PHONE_NUMBER)
                 .withSelection(Contacts.IN_VISIBLE_GROUP + "=1")
                 .withSortOrder(Contacts.SORT_KEY_PRIMARY)
-                .returnRow(1, "John", "John", "john", 1, 10,
+                .returnRow(1, "John", "John", "john", 1,
                         StatusUpdates.AVAILABLE, 23, "lk1", "john", 1)
-                .returnRow(2, "Jim", "Jim", "jim", 1, 8,
+                .returnRow(2, "Jim", "Jim", "jim", 1,
                         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();
+        mContactsProvider.expectQuery(ProviderStatus.CONTENT_URI)
+                .withProjection(ProviderStatus.STATUS, ProviderStatus.DATA1);
 
-        ListView listView = (ListView)activity.findViewById(android.R.id.list);
+        DefaultContactBrowseListFragment fragment = new DefaultContactBrowseListFragment();
+
+        LoaderManagingFragmentTestDelegate<Cursor> delegate =
+                new LoaderManagingFragmentTestDelegate<Cursor>();
+
+        // Divert loader registration to the delegate to ensure that loading is done synchronously
+        fragment.setDelegate(delegate);
+
+        // Fragment life cycle
+        fragment.onCreate(null);
+
+        // Instead of attaching the fragment to an activity, "attach" it to the target context
+        // of the instrumentation
+        fragment.setContext(mContext);
+
+        // Fragment life cycle
+        View view = fragment.onCreateView(LayoutInflater.from(mContext), null, null);
+
+        // Fragment life cycle
+        fragment.onStart();
+
+        // All loaders have been registered. Now perform the loading synchronously.
+        delegate.executeLoaders();
+
+        // Now we can assert that the data got loaded into the list.
+        ListView listView = (ListView)view.findViewById(android.R.id.list);
         ListAdapter adapter = listView.getAdapter();
-        assertEquals(3, adapter.getCount());
+        assertEquals(3, adapter.getCount());        // It has two items + header view
+
+        // Assert that all queries have been called
+        mSettingsProvider.verify();
+        mContactsProvider.verify();
     }
 }
diff --git a/tests/src/com/android/contacts/widget/LoaderManagingFragmentTestDelegate.java b/tests/src/com/android/contacts/widget/LoaderManagingFragmentTestDelegate.java
new file mode 100644
index 0000000..d5763a2
--- /dev/null
+++ b/tests/src/com/android/contacts/widget/LoaderManagingFragmentTestDelegate.java
@@ -0,0 +1,57 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.android.contacts.widget;
+
+import android.content.AsyncTaskLoader;
+import android.content.Loader;
+
+import java.util.LinkedHashMap;
+
+import junit.framework.Assert;
+
+/**
+ * A delegate of {@link InstrumentedLoaderManagingFragment} that performs
+ * synchronous loading on demand for unit testing.
+ */
+public class LoaderManagingFragmentTestDelegate<D> implements
+        InstrumentedLoaderManagingFragment.Delegate<D> {
+
+    // Using a linked hash map to get all loading done in a predictable order.
+    private LinkedHashMap<Integer, Loader<D>> mStartedLoaders =
+            new LinkedHashMap<Integer, Loader<D>>();
+
+    public void onStartLoading(Loader<D> loader) {
+        int id = loader.getId();
+        mStartedLoaders.put(id, loader);
+    }
+
+    /**
+     * Synchronously runs all started loaders.
+     */
+    public void executeLoaders() {
+        for (Loader<D> loader : mStartedLoaders.values()) {
+            executeLoader(loader);
+        }
+    }
+
+    /**
+     * Synchronously runs the specified loader.
+     */
+    public void executeLoader(int id) {
+        Loader<D> loader = mStartedLoaders.get(id);
+        if (loader == null) {
+            Assert.fail("Loader not started: " + id);
+        }
+        executeLoader(loader);
+    }
+
+    private void executeLoader(Loader<D> loader) {
+        if (loader instanceof AsyncTaskLoader) {
+            AsyncTaskLoader<D> atLoader = (AsyncTaskLoader<D>)loader;
+            D data = atLoader.loadInBackground();
+            atLoader.deliverResult(data);
+        } else {
+            loader.forceLoad();
+        }
+    }
+}
\ No newline at end of file