Move TestCommon sources into the test/src directory (1/2)

Bug 30759296

Change-Id: I1fcb3c70b4cc7fb1c0041dc1762ce06495045673
diff --git a/tests/Android.mk b/tests/Android.mk
index c1a5eb4..f018d3e 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -7,7 +7,7 @@
 
 LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
 
-src_dirs := src ../TestCommon/src
+src_dirs := src
 res_dirs := res res-common
 
 LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
diff --git a/tests/src/com/android/contacts/common/test/FragmentTestActivity.java b/tests/src/com/android/contacts/common/test/FragmentTestActivity.java
new file mode 100644
index 0000000..5ae2d95
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/FragmentTestActivity.java
@@ -0,0 +1,47 @@
+/*
+ * 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.common.test;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+/**
+ * An activity that is used for testing fragments.  A unit test starts this
+ * activity, adds a fragment and then tests the fragment.
+ */
+public class FragmentTestActivity extends Activity {
+
+    public final static int LAYOUT_ID = 1;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Normally fragment/activity onStart() methods will not be called when screen is locked.
+        // Use the following flags to ensure that activities can be show for testing.
+        final Window window = getWindow();
+        window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON |
+                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+
+        final FrameLayout layout = new FrameLayout(this);
+        layout.setId(LAYOUT_ID);
+        setContentView(layout);
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/IntegrationTestUtils.java b/tests/src/com/android/contacts/common/test/IntegrationTestUtils.java
new file mode 100644
index 0000000..5457128
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/IntegrationTestUtils.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2011 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.common.test;
+
+import static android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP;
+import static android.os.PowerManager.FULL_WAKE_LOCK;
+import static android.os.PowerManager.ON_AFTER_RELEASE;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.PowerManager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.google.common.base.Preconditions;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/** Some utility methods for making integration testing smoother. */
+@ThreadSafe
+public class IntegrationTestUtils {
+    private static final String TAG = "IntegrationTestUtils";
+
+    private final Instrumentation mInstrumentation;
+    private final Object mLock = new Object();
+    @GuardedBy("mLock") private PowerManager.WakeLock mWakeLock;
+
+    public IntegrationTestUtils(Instrumentation instrumentation) {
+        mInstrumentation = instrumentation;
+    }
+
+    /**
+     * Find a view by a given resource id, from the given activity, and click it, iff it is
+     * enabled according to {@link View#isEnabled()}.
+     */
+    public void clickButton(final Activity activity, final int buttonResourceId) throws Throwable {
+        runOnUiThreadAndGetTheResult(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                View view = activity.findViewById(buttonResourceId);
+                Assert.assertNotNull(view);
+                if (view.isEnabled()) {
+                    view.performClick();
+                }
+                return null;
+            }
+        });
+    }
+
+    /** Returns the result of running {@link TextView#getText()} on the ui thread. */
+    public CharSequence getText(final TextView view) throws Throwable {
+        return runOnUiThreadAndGetTheResult(new Callable<CharSequence>() {
+            @Override
+            public CharSequence call() {
+                return view.getText();
+            }
+        });
+    }
+
+    // TODO: Move this class and the appropriate documentation into a test library, having checked
+    // first to see if exactly this code already exists or not.
+    /**
+     * Execute a callable on the ui thread, returning its result synchronously.
+     * <p>
+     * Waits for an idle sync on the main thread (see {@link Instrumentation#waitForIdle(Runnable)})
+     * before executing this callable.
+     */
+    public <T> T runOnUiThreadAndGetTheResult(Callable<T> callable) throws Throwable {
+        FutureTask<T> future = new FutureTask<T>(callable);
+        mInstrumentation.waitForIdle(future);
+        try {
+            return future.get();
+        } catch (ExecutionException e) {
+            // Unwrap the cause of the exception and re-throw it.
+            throw e.getCause();
+        }
+    }
+
+    /**
+     * Wake up the screen, useful in tests that want or need the screen to be on.
+     * <p>
+     * This is usually called from setUp() for tests that require it.  After calling this method,
+     * {@link #releaseScreenWakeLock()} must be called, this is usually done from tearDown().
+     */
+    public void acquireScreenWakeLock(Context context) {
+        synchronized (mLock) {
+            Preconditions.checkState(mWakeLock == null, "mWakeLock was already held");
+            mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE))
+                    .newWakeLock(
+                            PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE | PowerManager.FULL_WAKE_LOCK, TAG);
+            mWakeLock.acquire();
+        }
+    }
+
+    /** Release the wake lock previously acquired with {@link #acquireScreenWakeLock(Context)}. */
+    public void releaseScreenWakeLock() {
+        synchronized (mLock) {
+            // We don't use Preconditions to force you to have acquired before release.
+            // This is because we don't want unnecessary exceptions in tearDown() since they'll
+            // typically mask the actual exception that happened during the test.
+            // The other reason is that this method is most likely to be called from tearDown(),
+            // which is invoked within a finally block, so it's not infrequently the case that
+            // the setUp() method fails before getting the lock, at which point we don't want
+            // to fail in tearDown().
+            if (mWakeLock != null) {
+                mWakeLock.release();
+                mWakeLock = null;
+            }
+        }
+    }
+
+    /**
+     * Gets all {@link TextView} objects whose {@link TextView#getText()} contains the given text as
+     * a substring.
+     */
+    public List<TextView> getTextViewsWithString(final Activity activity, final String text)
+            throws Throwable {
+        return getTextViewsWithString(getRootView(activity), text);
+    }
+
+    /**
+     * Gets all {@link TextView} objects whose {@link TextView#getText()} contains the given text as
+     * a substring for the given root view.
+     */
+    public List<TextView> getTextViewsWithString(final View rootView, final String text)
+            throws Throwable {
+        return runOnUiThreadAndGetTheResult(new Callable<List<TextView>>() {
+            @Override
+            public List<TextView> call() throws Exception {
+                List<TextView> matchingViews = new ArrayList<TextView>();
+                for (TextView textView : getAllViews(TextView.class, rootView)) {
+                    if (textView.getText().toString().contains(text)) {
+                        matchingViews.add(textView);
+                    }
+                }
+                return matchingViews;
+            }
+        });
+    }
+
+    /** Find the root view for a given activity. */
+    public static View getRootView(Activity activity) {
+        return activity.findViewById(android.R.id.content).getRootView();
+    }
+
+    /**
+     * Gets a list of all views of a given type, rooted at the given parent.
+     * <p>
+     * This method will recurse down through all {@link ViewGroup} instances looking for
+     * {@link View} instances of the supplied class type. Specifically it will use the
+     * {@link Class#isAssignableFrom(Class)} method as the test for which views to add to the list,
+     * so if you provide {@code View.class} as your type, you will get every view. The parent itself
+     * will be included also, should it be of the right type.
+     * <p>
+     * This call manipulates the ui, and as such should only be called from the application's main
+     * thread.
+     */
+    private static <T extends View> List<T> getAllViews(final Class<T> clazz, final View parent) {
+        List<T> results = new ArrayList<T>();
+        if (parent.getClass().equals(clazz)) {
+            results.add(clazz.cast(parent));
+        }
+        if (parent instanceof ViewGroup) {
+            ViewGroup viewGroup = (ViewGroup) parent;
+            for (int i = 0; i < viewGroup.getChildCount(); ++i) {
+                results.addAll(getAllViews(clazz, viewGroup.getChildAt(i)));
+            }
+        }
+        return results;
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/LaunchPerformanceBase.java b/tests/src/com/android/contacts/common/test/LaunchPerformanceBase.java
new file mode 100644
index 0000000..a2ebde3
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/LaunchPerformanceBase.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007 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.common.test;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.os.Bundle;
+
+
+/**
+ * Base class for all launch performance Instrumentation classes.
+ */
+public class LaunchPerformanceBase extends Instrumentation {
+
+    public static final String LOG_TAG = "Launch Performance";
+
+    protected Bundle mResults;
+    protected Intent mIntent;
+
+    public LaunchPerformanceBase() {
+        mResults = new Bundle();
+        mIntent = new Intent(Intent.ACTION_MAIN);
+        mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        setAutomaticPerformanceSnapshots();
+    }
+
+    /**
+     * Launches intent, and waits for idle before returning.
+     *
+     * @hide
+     */
+    protected void LaunchApp() {
+        startActivitySync(mIntent);
+        waitForIdleSync();
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/common/test/mocks/ContactsMockContext.java
new file mode 100644
index 0000000..c72fe3d
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/ContactsMockContext.java
@@ -0,0 +1,96 @@
+/*
+ * 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.common.test.mocks;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.provider.ContactsContract;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+
+/**
+ * A mock context for contacts 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 ContactsMockPackageManager mPackageManager;
+    private MockContentResolver mContentResolver;
+    private MockContentProvider mContactsProvider;
+    private MockContentProvider mSettingsProvider;
+    private Intent mIntentForStartActivity;
+
+    public ContactsMockContext(Context base) {
+        this(base, ContactsContract.AUTHORITY);
+    }
+
+    public ContactsMockContext(Context base, String authority) {
+        super(base);
+        mPackageManager = new ContactsMockPackageManager();
+        mContentResolver = new MockContentResolver();
+        mContactsProvider = new MockContentProvider();
+        mContentResolver.addProvider(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;
+    }
+
+    @Override
+    public PackageManager getPackageManager() {
+        return mPackageManager;
+    }
+
+    @Override
+    public Context getApplicationContext() {
+        return this;
+    }
+
+    /**
+     * Instead of actually sending Intent, this method just remembers what Intent was supplied last.
+     * You can check the content via {@link #getIntentForStartActivity()} for verification.
+     */
+    @Override
+    public void startActivity(Intent intent) {
+        mIntentForStartActivity = intent;
+    }
+
+    public Intent getIntentForStartActivity() {
+        return mIntentForStartActivity;
+    }
+
+    public void verify() {
+        mContactsProvider.verify();
+        mSettingsProvider.verify();
+    }
+
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/ContactsMockPackageManager.java b/tests/src/com/android/contacts/common/test/mocks/ContactsMockPackageManager.java
new file mode 100644
index 0000000..a1557ff
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/ContactsMockPackageManager.java
@@ -0,0 +1,45 @@
+/*
+ * 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.common.test.mocks;
+
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.test.mock.MockPackageManager;
+
+/**
+ */
+public class ContactsMockPackageManager extends MockPackageManager {
+    public ContactsMockPackageManager() {
+    }
+
+    @Override
+    public Drawable getActivityLogo(ComponentName activityName) throws NameNotFoundException {
+        return new ColorDrawable();
+    }
+
+    @Override
+    public Drawable getActivityIcon(ComponentName activityName) {
+        return new ColorDrawable();
+    }
+
+    @Override
+    public Drawable getDrawable(String packageName, int resid, ApplicationInfo appInfo) {
+        // TODO: make programmable
+        return new ColorDrawable();
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
new file mode 100644
index 0000000..b46c49d
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
@@ -0,0 +1,93 @@
+/*
+ * 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.common.test.mocks;
+
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.BaseAccountType;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A mock {@link AccountTypeManager} class.
+ */
+public class MockAccountTypeManager extends AccountTypeManager {
+
+    public AccountType[] mTypes;
+    public AccountWithDataSet[] mAccounts;
+
+    public MockAccountTypeManager(AccountType[] types, AccountWithDataSet[] accounts) {
+        this.mTypes = types;
+        this.mAccounts = accounts;
+    }
+
+    @Override
+    public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
+        // Add fallback accountType to mimic the behavior of AccountTypeManagerImpl
+        AccountType mFallbackAccountType = new BaseAccountType() {
+            @Override
+            public boolean areContactsWritable() {
+                return false;
+            }
+        };
+        mFallbackAccountType.accountType = "fallback";
+        for (AccountType type : mTypes) {
+            if (Objects.equal(accountTypeWithDataSet.accountType, type.accountType)
+                    && Objects.equal(accountTypeWithDataSet.dataSet, type.dataSet)) {
+                return type;
+            }
+        }
+        return mFallbackAccountType;
+    }
+
+    @Override
+    public List<AccountWithDataSet> getAccounts(boolean writableOnly) {
+        return Arrays.asList(mAccounts);
+    }
+
+    @Override
+    public void sortAccounts(AccountWithDataSet account) {}
+
+    @Override
+    public List<AccountWithDataSet> getGroupWritableAccounts() {
+        return Arrays.asList(mAccounts);
+    }
+
+    @Override
+    public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+        return Maps.newHashMap(); // Always returns empty
+    }
+
+    @Override
+    public List<AccountType> getAccountTypes(boolean writableOnly) {
+        final List<AccountType> ret = Lists.newArrayList();
+        synchronized (this) {
+            for (AccountType type : mTypes) {
+                if (!writableOnly || type.areContactsWritable()) {
+                    ret.add(type);
+                }
+            }
+        }
+        return ret;
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java b/tests/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java
new file mode 100644
index 0000000..db8f06f
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockContactPhotoManager.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2011 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.common.test.mocks;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.contacts.common.ContactPhotoManager;
+
+/**
+ * A photo preloader that always uses the "no contact" picture and never executes any real
+ * db queries
+ */
+public class MockContactPhotoManager extends ContactPhotoManager {
+    @Override
+    public void loadThumbnail(ImageView view, long photoId, boolean darkTheme, boolean isCircular,
+            DefaultImageRequest defaultImageRequest, DefaultImageProvider defaultProvider) {
+        defaultProvider.applyDefaultImage(view, -1, darkTheme, null);
+    }
+
+    @Override
+    public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
+            boolean isCircular, DefaultImageRequest defaultImageRequest,
+            DefaultImageProvider defaultProvider) {
+        defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, null);
+    }
+
+    @Override
+    public void removePhoto(ImageView view) {
+        view.setImageDrawable(null);
+    }
+
+    @Override
+    public void cancelPendingRequests(View fragmentRootView) {
+    }
+
+    @Override
+    public void pause() {
+    }
+
+    @Override
+    public void resume() {
+    }
+
+    @Override
+    public void refreshCache() {
+    }
+
+    @Override
+    public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
+    }
+
+    @Override
+    public void preloadPhotosInBackground() {
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockContentProvider.java b/tests/src/com/android/contacts/common/test/mocks/MockContentProvider.java
new file mode 100644
index 0000000..335e8d2
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockContentProvider.java
@@ -0,0 +1,659 @@
+/*
+ * 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.common.test.mocks;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockContentProvider extends android.test.mock.MockContentProvider {
+    private static final String TAG = "MockContentProvider";
+
+    public static class Query {
+
+        private final Uri mUri;
+        private String[] mProjection;
+        private String[] mDefaultProjection;
+        private String mSelection;
+        private String[] mSelectionArgs;
+        private String mSortOrder;
+        private List<Object> mRows = new ArrayList<>();
+        private boolean mAnyProjection;
+        private boolean mAnySelection;
+        private boolean mAnySortOrder;
+        private boolean mAnyNumberOfTimes;
+
+        private boolean mExecuted;
+
+        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 withAnyProjection() {
+            mAnyProjection = true;
+            return this;
+        }
+
+        public Query withSelection(String selection, String... selectionArgs) {
+            mSelection = selection;
+            mSelectionArgs = selectionArgs;
+            return this;
+        }
+
+        public Query withAnySelection() {
+            mAnySelection = true;
+            return this;
+        }
+
+        public Query withSortOrder(String sortOrder) {
+            mSortOrder = sortOrder;
+            return this;
+        }
+
+        public Query withAnySortOrder() {
+            mAnySortOrder = true;
+            return this;
+        }
+
+        public Query returnRow(ContentValues values) {
+            mRows.add(values);
+            return this;
+        }
+
+        public Query returnRow(Object... row) {
+            mRows.add(row);
+            return this;
+        }
+
+        public Query returnEmptyCursor() {
+            mRows.clear();
+            return this;
+        }
+
+        public Query anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        public boolean equals(Uri uri, String[] projection, String selection,
+                String[] selectionArgs, String sortOrder) {
+            if (!uri.equals(mUri)) {
+                return false;
+            }
+
+            if (!mAnyProjection && !Arrays.equals(projection, mProjection)) {
+                return false;
+            }
+
+            if (!mAnySelection && !Objects.equals(selection, mSelection)) {
+                return false;
+            }
+
+            if (!mAnySelection && !Arrays.equals(selectionArgs, mSelectionArgs)) {
+                return false;
+            }
+
+            if (!mAnySortOrder && !Objects.equals(sortOrder, mSortOrder)) {
+                return false;
+            }
+
+            return true;
+        }
+
+        public Cursor getResult(String[] projection) {
+            String[] columnNames;
+            if (mAnyProjection) {
+                columnNames = projection;
+            } else {
+                columnNames = mProjection != null ? mProjection : mDefaultProjection;
+            }
+
+            MatrixCursor cursor = new MatrixCursor(columnNames);
+            for (Object row : mRows) {
+                if (row instanceof Object[]) {
+                    cursor.addRow((Object[]) row);
+                } else {
+                    ContentValues values = (ContentValues) row;
+                    Object[] columns = new Object[projection.length];
+                    for (int i = 0; i < projection.length; i++) {
+                        columns[i] = values.get(projection[i]);
+                    }
+                    cursor.addRow(columns);
+                }
+            }
+            return cursor;
+        }
+    }
+
+    public static class TypeQuery {
+        private final Uri mUri;
+        private final String mType;
+
+        public TypeQuery(Uri uri, String type) {
+            mUri = uri;
+            mType = type;
+        }
+
+        public Uri getUri() {
+            return mUri;
+        }
+
+        public String getType() {
+            return mType;
+        }
+
+        @Override
+        public String toString() {
+            return mUri + " --> " + mType;
+        }
+
+        public boolean equals(Uri uri) {
+            return getUri().equals(uri);
+        }
+    }
+
+    public static class Insert {
+        private final Uri mUri;
+        private final ContentValues mContentValues;
+        private final Uri mResultUri;
+        private boolean mAnyNumberOfTimes;
+        private boolean mIsExecuted;
+
+        /**
+         * Creates a new Insert to expect.
+         *
+         * @param uri the uri of the insertion request.
+         * @param contentValues the ContentValues to insert.
+         * @param resultUri the {@link Uri} for the newly inserted item.
+         * @throws NullPointerException if any parameter is {@code null}.
+         */
+        public Insert(Uri uri, ContentValues contentValues, Uri resultUri) {
+            mUri = Preconditions.checkNotNull(uri);
+            mContentValues = Preconditions.checkNotNull(contentValues);
+            mResultUri = Preconditions.checkNotNull(resultUri);
+        }
+
+        /**
+         * Causes this insert expectation to be useable for mutliple calls to insert, rather than
+         * just one.
+         *
+         * @return this
+         */
+        public Insert anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        private boolean equals(Uri uri, ContentValues contentValues) {
+            return mUri.equals(uri) && mContentValues.equals(contentValues);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Insert insert = (Insert) o;
+            return mAnyNumberOfTimes == insert.mAnyNumberOfTimes &&
+                    mIsExecuted == insert.mIsExecuted &&
+                    Objects.equals(mUri, insert.mUri) &&
+                    Objects.equals(mContentValues, insert.mContentValues) &&
+                    Objects.equals(mResultUri, insert.mResultUri);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mUri, mContentValues, mResultUri, mAnyNumberOfTimes, mIsExecuted);
+        }
+
+        @Override
+        public String toString() {
+            return "Insert{" +
+                    "mUri=" + mUri +
+                    ", mContentValues=" + mContentValues +
+                    ", mResultUri=" + mResultUri +
+                    ", mAnyNumberOfTimes=" + mAnyNumberOfTimes +
+                    ", mIsExecuted=" + mIsExecuted +
+                    '}';
+        }
+    }
+
+    public static class Delete {
+        private final Uri mUri;
+
+        private boolean mAnyNumberOfTimes;
+        private boolean mAnySelection;
+        @Nullable private String mSelection;
+        @Nullable private String[] mSelectionArgs;
+        private boolean mIsExecuted;
+        private int mRowsAffected;
+
+        /**
+         * Creates a new Delete to expect.
+         * @param uri the uri of the delete request.
+         * @throws NullPointerException if uri is {@code null}.
+         */
+        public Delete(Uri uri) {
+            mUri = Preconditions.checkNotNull(uri);
+        }
+
+        /**
+         * Sets the given information as expected selection arguments.
+         *
+         * @param selection The selection to expect.
+         * @param selectionArgs The selection args to expect.
+         * @return this.
+         */
+        public Delete withSelection(String selection, @Nullable String[] selectionArgs) {
+            mSelection = Preconditions.checkNotNull(selection);
+            mSelectionArgs = selectionArgs;
+            mAnySelection = false;
+            return this;
+        }
+
+        /**
+         * Sets this delete to expect any selection arguments.
+         *
+         * @return this.
+         */
+        public Delete withAnySelection() {
+            mAnySelection = true;
+            return this;
+        }
+
+        /**
+         * Sets this delete to return the given number of rows affected.
+         *
+         * @param rowsAffected The value to return when this expected delete is executed.
+         * @return this.
+         */
+        public Delete returnRowsAffected(int rowsAffected) {
+            mRowsAffected = rowsAffected;
+            return this;
+        }
+
+        /**
+         * Causes this delete expectation to be useable for multiple calls to delete, rather than
+         * just one.
+         *
+         * @return this.
+         */
+        public Delete anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        private boolean equals(Uri uri, String selection, String[] selectionArgs) {
+            return mUri.equals(uri) && Objects.equals(mSelection, selection)
+                    && Arrays.equals(mSelectionArgs, selectionArgs);
+        }
+    }
+
+    public static class Update {
+        private final Uri mUri;
+        private final ContentValues mContentValues;
+        @Nullable private String mSelection;
+        @Nullable private String[] mSelectionArgs;
+        private boolean mAnyNumberOfTimes;
+        private boolean mIsExecuted;
+        private int mRowsAffected;
+
+        /**
+         * Creates a new Update to expect.
+         *
+         * @param uri the uri of the update request.
+         * @param contentValues the ContentValues to update.
+         *
+         * @throws NullPointerException if any parameter is {@code null}.
+         */
+        public Update(Uri uri,
+                      ContentValues contentValues,
+                      @Nullable String selection,
+                      @Nullable String[] selectionArgs) {
+            mUri = Preconditions.checkNotNull(uri);
+            mContentValues = Preconditions.checkNotNull(contentValues);
+            mSelection = selection;
+            mSelectionArgs = selectionArgs;
+        }
+
+        /**
+         * Causes this update expectation to be useable for mutliple calls to update, rather than
+         * just one.
+         *
+         * @return this
+         */
+        public Update anyNumberOfTimes() {
+            mAnyNumberOfTimes = true;
+            return this;
+        }
+
+        /**
+         * Sets this update to return the given number of rows affected.
+         *
+         * @param rowsAffected The value to return when this expected update is executed.
+         * @return this.
+         */
+        public Update returnRowsAffected(int rowsAffected) {
+            mRowsAffected = rowsAffected;
+            return this;
+        }
+
+        private boolean equals(Uri uri,
+                               ContentValues contentValues,
+                               @Nullable String selection,
+                               @Nullable String[] selectionArgs) {
+            return mUri.equals(uri) && mContentValues.equals(contentValues) &&
+                    Objects.equals(mSelection, selection) &&
+                    Objects.equals(mSelectionArgs, selectionArgs);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            Update update = (Update) o;
+            return mAnyNumberOfTimes == update.mAnyNumberOfTimes &&
+                    mIsExecuted == update.mIsExecuted &&
+                    Objects.equals(mUri, update.mUri) &&
+                    Objects.equals(mContentValues, update.mContentValues) &&
+                    Objects.equals(mSelection, update.mSelection) &&
+                    Objects.equals(mSelectionArgs, update.mSelectionArgs);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mUri, mContentValues, mAnyNumberOfTimes, mIsExecuted, mSelection,
+                    mSelectionArgs);
+        }
+
+        @Override
+        public String toString() {
+            return "Update{" +
+                    "mUri=" + mUri +
+                    ", mContentValues=" + mContentValues +
+                    ", mAnyNumberOfTimes=" + mAnyNumberOfTimes +
+                    ", mIsExecuted=" + mIsExecuted +
+                    ", mSelection=" + mSelection +
+                    ", mSelectionArgs=" + mSelectionArgs +
+                    '}';
+        }
+    }
+
+    private List<Query> mExpectedQueries = new ArrayList<>();
+    private Map<Uri, String> mExpectedTypeQueries = Maps.newHashMap();
+    private List<Insert> mExpectedInserts = new ArrayList<>();
+    private List<Delete> mExpectedDeletes = new ArrayList<>();
+    private List<Update> mExpectedUpdates = new ArrayList<>();
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    public Query expectQuery(Uri contentUri) {
+        Query query = new Query(contentUri);
+        mExpectedQueries.add(query);
+        return query;
+    }
+
+    public void expectTypeQuery(Uri uri, String type) {
+        mExpectedTypeQueries.put(uri, type);
+    }
+
+    public void expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri) {
+        mExpectedInserts.add(new Insert(contentUri, contentValues, resultUri));
+    }
+
+    public Update expectUpdate(Uri contentUri,
+                               ContentValues contentValues,
+                               @Nullable String selection,
+                               @Nullable String[] selectionArgs) {
+        Update update = new Update(contentUri, contentValues, selection, selectionArgs);
+        mExpectedUpdates.add(update);
+        return update;
+    }
+
+    public Delete expectDelete(Uri contentUri) {
+        Delete delete = new Delete(contentUri);
+        mExpectedDeletes.add(delete);
+        return delete;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        if (mExpectedQueries.isEmpty()) {
+            Assert.fail("Unexpected query: Actual:"
+                    + queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        }
+
+        for (Iterator<Query> iterator = mExpectedQueries.iterator(); iterator.hasNext();) {
+            Query query = iterator.next();
+            if (query.equals(uri, projection, selection, selectionArgs, sortOrder)) {
+                query.mExecuted = true;
+                if (!query.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return query.getResult(projection);
+            }
+        }
+
+        Assert.fail("Incorrect query. Expected one of: " + mExpectedQueries + ". Actual: " +
+                queryToString(uri, projection, selection, selectionArgs, sortOrder));
+        return null;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        if (mExpectedTypeQueries.isEmpty()) {
+            Assert.fail("Unexpected getType query: " + uri);
+        }
+
+        String mimeType = mExpectedTypeQueries.get(uri);
+        if (mimeType != null) {
+            return mimeType;
+        }
+
+        Assert.fail("Unknown mime type for: " + uri);
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        if (mExpectedInserts.isEmpty()) {
+            Assert.fail("Unexpected insert. Actual: " + insertToString(uri, values));
+        }
+        for (Iterator<Insert> iterator = mExpectedInserts.iterator(); iterator.hasNext(); ) {
+            Insert insert = iterator.next();
+            if (insert.equals(uri, values)) {
+                insert.mIsExecuted = true;
+                if (!insert.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return insert.mResultUri;
+            }
+        }
+
+        Assert.fail("Incorrect insert. Expected one of: " + mExpectedInserts + ". Actual: "
+                + insertToString(uri, values));
+        return null;
+    }
+
+    private String insertToString(Uri uri, ContentValues contentValues) {
+        return "Insert { uri=" + uri + ", contentValues=" + contentValues + '}';
+    }
+
+    @Override
+    public int update(Uri uri,
+                      ContentValues values,
+                      @Nullable String selection,
+                      @Nullable String[] selectionArgs) {
+        if (mExpectedUpdates.isEmpty()) {
+            Assert.fail("Unexpected update. Actual: "
+                    + updateToString(uri, values, selection, selectionArgs));
+        }
+        for (Iterator<Update> iterator = mExpectedUpdates.iterator(); iterator.hasNext(); ) {
+            Update update = iterator.next();
+            if (update.equals(uri, values, selection, selectionArgs)) {
+                update.mIsExecuted = true;
+                if (!update.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return update.mRowsAffected;
+            }
+        }
+
+        Assert.fail("Incorrect update. Expected one of: " + mExpectedUpdates + ". Actual: "
+                + updateToString(uri, values, selection, selectionArgs));
+        return - 1;
+    }
+
+    private String updateToString(Uri uri,
+                                  ContentValues contentValues,
+                                  @Nullable String selection,
+                                  @Nullable String[] selectionArgs) {
+        return "Update { uri=" + uri + ", contentValues=" + contentValues + ", selection=" +
+                selection + ", selectionArgs" + Arrays.toString(selectionArgs) + '}';
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        if (mExpectedDeletes.isEmpty()) {
+            Assert.fail("Unexpected delete. Actual: " + deleteToString(uri, selection,
+                    selectionArgs));
+        }
+        for (Iterator<Delete> iterator = mExpectedDeletes.iterator(); iterator.hasNext(); ) {
+            Delete delete = iterator.next();
+            if (delete.equals(uri, selection, selectionArgs)) {
+                delete.mIsExecuted = true;
+                if (!delete.mAnyNumberOfTimes) {
+                    iterator.remove();
+                }
+                return delete.mRowsAffected;
+            }
+        }
+        Assert.fail("Incorrect delete. Expected one of: " + mExpectedDeletes + ". Actual: "
+                + deleteToString(uri, selection, selectionArgs));
+        return -1;
+    }
+
+    private String deleteToString(Uri uri, String selection, String[] selectionArgs) {
+        return "Delete { uri=" + uri + ", selection=" + selection + ", selectionArgs"
+                + Arrays.toString(selectionArgs) + '}';
+    }
+
+    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 (selectionArgs != null) {
+                sb.append(Arrays.toString(selectionArgs));
+            } else {
+                sb.append("[]");
+            }
+        }
+        if (sortOrder != null) {
+            sb.append(" sort: '").append(sortOrder).append("'");
+        }
+        return sb.toString();
+    }
+
+    public void verify() {
+        verifyQueries();
+        verifyInserts();
+        verifyDeletes();
+    }
+
+    private void verifyQueries() {
+        List<Query> missedQueries = new ArrayList<>();
+        for (Query query : mExpectedQueries) {
+            if (!query.mExecuted) {
+                missedQueries.add(query);
+            }
+        }
+        Assert.assertTrue("Not all expected queries have been called: " + missedQueries,
+                missedQueries.isEmpty());
+    }
+
+    private void verifyInserts() {
+        List<Insert> missedInserts = new ArrayList<>();
+        for (Insert insert : mExpectedInserts) {
+            if (!insert.mIsExecuted) {
+                missedInserts.add(insert);
+            }
+        }
+        Assert.assertTrue("Not all expected inserts have been called: " + missedInserts,
+                missedInserts.isEmpty());
+    }
+
+    private void verifyDeletes() {
+        List<Delete> missedDeletes = new ArrayList<>();
+        for (Delete delete : mExpectedDeletes) {
+            if (!delete.mIsExecuted) {
+                missedDeletes.add(delete);
+            }
+        }
+        Assert.assertTrue("Not all expected deletes have been called: " + missedDeletes,
+                missedDeletes.isEmpty());
+    }
+}
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java b/tests/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java
new file mode 100644
index 0000000..13d035e
--- /dev/null
+++ b/tests/src/com/android/contacts/common/test/mocks/MockSharedPreferences.java
@@ -0,0 +1,149 @@
+/*
+ * 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.common.test.mocks;
+
+import android.content.SharedPreferences;
+
+import com.google.common.collect.Maps;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * A programmable mock content provider.
+ */
+public class MockSharedPreferences implements SharedPreferences, SharedPreferences.Editor {
+
+    private HashMap<String, Object> mValues = Maps.newHashMap();
+    private HashMap<String, Object> mTempValues = Maps.newHashMap();
+
+    public Editor edit() {
+        return this;
+    }
+
+    public boolean contains(String key) {
+        return mValues.containsKey(key);
+    }
+
+    public Map<String, ?> getAll() {
+        return new HashMap<String, Object>(mValues);
+    }
+
+    public boolean getBoolean(String key, boolean defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Boolean)mValues.get(key)).booleanValue();
+        }
+        return defValue;
+    }
+
+    public float getFloat(String key, float defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Float)mValues.get(key)).floatValue();
+        }
+        return defValue;
+    }
+
+    public int getInt(String key, int defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Integer)mValues.get(key)).intValue();
+        }
+        return defValue;
+    }
+
+    public long getLong(String key, long defValue) {
+        if (mValues.containsKey(key)) {
+            return ((Long)mValues.get(key)).longValue();
+        }
+        return defValue;
+    }
+
+    public String getString(String key, String defValue) {
+        if (mValues.containsKey(key))
+            return (String)mValues.get(key);
+        return defValue;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Set<String> getStringSet(String key, Set<String> defValues) {
+        if (mValues.containsKey(key)) {
+            return (Set<String>) mValues.get(key);
+        }
+        return defValues;
+    }
+
+    public void registerOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    public void unregisterOnSharedPreferenceChangeListener(
+            OnSharedPreferenceChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Editor putBoolean(String key, boolean value) {
+        mTempValues.put(key, Boolean.valueOf(value));
+        return this;
+    }
+
+    public Editor putFloat(String key, float value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putInt(String key, int value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putLong(String key, long value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putString(String key, String value) {
+        mTempValues.put(key, value);
+        return this;
+    }
+
+    public Editor putStringSet(String key, Set<String> values) {
+        mTempValues.put(key, values);
+        return this;
+    }
+
+    public Editor remove(String key) {
+        mTempValues.remove(key);
+        return this;
+    }
+
+    public Editor clear() {
+        mTempValues.clear();
+        return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    public boolean commit() {
+        mValues = (HashMap<String, Object>)mTempValues.clone();
+        return true;
+    }
+
+    public void apply() {
+        commit();
+    }
+}