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