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();
+ }
+}