Initial checkin of AOSP Messaging app.

b/23110861

Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..f3f4752
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,39 @@
+#  Copyright (C) 2015 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := messagingtests
+
+LOCAL_INSTRUMENTATION_FOR := messaging
+
+LOCAL_JACK_ENABLED := disabled
+
+# Matching ../Android.mk
+LOCAL_SDK_VERSION := current
+
+LOCAL_CERTIFICATE := platform
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    mockito-target
+
+include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..867d8e1
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2015 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.messaging.test" >
+
+    <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="19"/>
+
+    <application android:label="Messaging Tests" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:label="Messaging Tests"
+        android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.messaging"
+        android:windowSoftInputMode="stateHidden|adjustResize" />
+</manifest>
diff --git a/tests/src/com/android/messaging/BugleTestCase.java b/tests/src/com/android/messaging/BugleTestCase.java
new file mode 100644
index 0000000..64c6c86
--- /dev/null
+++ b/tests/src/com/android/messaging/BugleTestCase.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2015 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.messaging;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+
+/*
+ * Base class for service tests that takes care of housekeeping that is common amongst basic test
+ * cases.
+ */
+public abstract class BugleTestCase extends AndroidTestCase {
+
+    static {
+        // Set flag during loading of test cases to prevent application initialization starting
+        setTestsRunning();
+    }
+
+    public static void setTestsRunning() {
+        BugleApplication.setTestsRunning();
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        TestUtil.testSetup(super.getContext(), this);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        TestUtil.testTeardown(this);
+    }
+
+    @Override
+    public Context getContext() {
+        // This doesn't really get the "application context" - just the fake context
+        // that the factory has been initialized with for each test case.
+        return Factory.get().getApplicationContext();
+    }
+
+    public Context getTestContext() {
+        return super.getContext();
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/messaging/FakeContentProvider.java b/tests/src/com/android/messaging/FakeContentProvider.java
new file mode 100644
index 0000000..53c29ba
--- /dev/null
+++ b/tests/src/com/android/messaging/FakeContentProvider.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 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.messaging;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.support.v4.util.SimpleArrayMap;
+import android.text.TextUtils;
+
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.util.LogUtil;
+
+import java.util.ArrayList;
+
+public class FakeContentProvider extends ContentProvider {
+
+    private static class ContentOverride {
+        private final String uri;
+        private final String where;
+        private final String args;
+        private final String[] columns;
+        private final Object[][] data;
+
+        ContentOverride(final String uri, final String where, final String args,
+                final String[] columns, final Object[][] data) {
+            this.uri = uri;
+            this.where = where;
+            this.args = args;
+            this.columns = columns;
+            this.data = data;
+        }
+
+        boolean match(final String uri, final String where, final String[] args) {
+            if (!this.uri.equals(uri) || !TextUtils.equals(this.where, where)) {
+                return false;
+            }
+
+            if (this.args == null || args == null) {
+                return this.args == null && args == null;
+            }
+
+            return this.args.equals(TextUtils.join(";", args));
+        }
+    }
+
+    private final Context mGlobalContext;
+    private final ArrayList<ContentOverride> mOverrides = new ArrayList<ContentOverride>();
+    private final SimpleArrayMap<String, String> mTypes = new SimpleArrayMap<String, String>();
+    private final ContentProviderClient mProvider;
+    private final Uri mUri;
+
+    public FakeContentProvider(final Context context, final Uri uri, final boolean canDelegate) {
+        mGlobalContext = context;
+        mUri = uri;
+        if (canDelegate) {
+            mProvider = mGlobalContext.getContentResolver().acquireContentProviderClient(mUri);
+        } else {
+            mProvider = null;
+        }
+
+        ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.authority = uri.getAuthority();
+
+        this.attachInfo(mGlobalContext, providerInfo);
+    }
+
+    public void addOverrideData(final Uri uri, final String where, final String args,
+            final String[] columns, final Object[][] data) {
+        mOverrides.add(new ContentOverride(uri.toString(), where, args, columns, data));
+    }
+
+    public void addOverrideType(final Uri uri, final String type) {
+        mTypes.put(uri.toString(), type);
+    }
+
+    @Override
+    public boolean onCreate() {
+        return false;
+    }
+
+    @Override
+    public void shutdown() {
+        if (mProvider != null) {
+            mProvider.release();
+        }
+    }
+
+    @Override
+    public Cursor query(final Uri uri, final String[] projection, final String selection,
+            final String[] selectionArgs, final String sortOrder) {
+        LogUtil.w(LogUtil.BUGLE_TAG, "FakeContentProvider: query " + uri.toString()
+                + " for " + (projection == null ? null : TextUtils.join(",", projection))
+                + " where " + selection
+                + " with " + (selectionArgs == null ? null : TextUtils.join(";", selectionArgs)));
+
+        for(final ContentOverride content : mOverrides) {
+            if (content.match(uri.toString(), selection, selectionArgs)) {
+                return new FakeCursor(projection, content.columns, content.data);
+            }
+        }
+        if (mProvider != null) {
+            try {
+                LogUtil.w(LogUtil.BUGLE_TAG, "FakeContentProvider: delgating");
+
+                final Cursor cursor = mProvider.query(uri, projection, selection, selectionArgs,
+                        sortOrder);
+
+                LogUtil.w(LogUtil.BUGLE_TAG, "FakeContentProvider: response size "
+                        + cursor.getCount() + " contains " + TextUtils.join(",",
+                                cursor.getColumnNames()) + " type(0) " + cursor.getType(0));
+
+                return cursor;
+            } catch (final RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public String getType(final Uri uri) {
+        String type = mTypes.get(uri.toString());
+        if (type == null) {
+            try {
+                type = mProvider.getType(uri);
+            } catch (final RemoteException e) {
+                e.printStackTrace();
+            }
+        }
+        return type;
+    }
+
+    @Override
+    public Uri insert(final Uri uri, final ContentValues values) {
+        // TODO: Add code to track insert operations and return correct status
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int delete(final Uri uri, final String selection, final String[] selectionArgs) {
+        // TODO: Add code to track delete operations and return correct status
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(final Uri uri, final ContentValues values, final String selection,
+            final String[] selectionArgs) {
+        // TODO: Add code to track update operations and return correct status
+        throw new UnsupportedOperationException();
+    }
+
+    public Bundle call(final String callingPkg, final String method, final String arg,
+            final Bundle extras) {
+        return null;
+    }
+}
diff --git a/tests/src/com/android/messaging/FakeContext.java b/tests/src/com/android/messaging/FakeContext.java
new file mode 100644
index 0000000..671e0f3
--- /dev/null
+++ b/tests/src/com/android/messaging/FakeContext.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2015 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.messaging;
+
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.test.RenamingDelegatingContext;
+import android.test.mock.MockContentResolver;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+public class FakeContext extends RenamingDelegatingContext {
+    private static final String TAG = "FakeContext";
+
+    public interface FakeContextHost {
+        public String getServiceClassName();
+        public void startServiceForStub(Intent intent);
+        public void onStartCommandForStub(Intent intent, int flags, int startid);
+    }
+
+    ArrayList<Intent> mStartedIntents;
+    boolean mServiceStarted = false;
+    private final FakeContextHost mService;
+    private final MockContentResolver mContentResolver;
+
+
+    public FakeContext(final Context context, final FakeContextHost service) {
+        super(context, "test_");
+        mService = service;
+        mStartedIntents = new ArrayList<Intent>();
+        mContentResolver = new MockContentResolver();
+    }
+
+    public FakeContext(final Context context) {
+        this(context, null);
+    }
+
+    public ArrayList<Intent> extractIntents() {
+        final ArrayList<Intent> intents = mStartedIntents;
+        mStartedIntents = new ArrayList<Intent>();
+        return intents;
+    }
+
+    @Override
+    public ComponentName startService(final Intent intent) {
+        // Record that a startService occurred with the intent that was passed.
+        Log.d(TAG, "MockContext receiving startService. intent=" + intent.toString());
+        mStartedIntents.add(intent);
+        if (mService == null) {
+            return super.startService(intent);
+        } else if (intent.getComponent() != null &&
+                intent.getComponent().getClassName().equals(mService.getServiceClassName())) {
+            if (!mServiceStarted) {
+                Log.d(TAG, "MockContext first start service.");
+                mService.startServiceForStub(intent);
+            } else {
+                Log.d(TAG, "MockContext not first start service. Calling onStartCommand.");
+                mService.onStartCommandForStub(intent, 0, 0);
+            }
+            mServiceStarted = true;
+            return new ComponentName(this, intent.getComponent().getClassName());
+        }
+        return null;
+    }
+
+    @Override
+    public ContentResolver getContentResolver() {
+        // If you want to use a content provider in your test, then you need to add it
+        // explicitly.
+        return mContentResolver;
+    }
+
+    public void addContentProvider(final String name, final ContentProvider provider) {
+        mContentResolver.addProvider(name, provider);
+    }
+
+    public void addDefaultProvider(final Context context, final Uri uri) {
+        final FakeContentProvider provider = new FakeContentProvider(context, uri, true);
+        mContentResolver.addProvider(uri.getAuthority(), provider);
+    }
+
+}
diff --git a/tests/src/com/android/messaging/FakeFactory.java b/tests/src/com/android/messaging/FakeFactory.java
new file mode 100644
index 0000000..41ede77
--- /dev/null
+++ b/tests/src/com/android/messaging/FakeFactory.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2015 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.messaging;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.MemoryCacheManager;
+import com.android.messaging.datamodel.ParticipantRefresh.ContactContentObserver;
+import com.android.messaging.datamodel.media.MediaCacheManager;
+import com.android.messaging.datamodel.media.MediaResourceManager;
+import com.android.messaging.sms.ApnDatabase;
+import com.android.messaging.sms.BugleCarrierConfigValuesLoader;
+import com.android.messaging.ui.UIIntents;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.BugleGservices;
+import com.android.messaging.util.BuglePrefs;
+import com.android.messaging.util.FakeBugleGservices;
+import com.android.messaging.util.FakeBuglePrefs;
+import com.android.messaging.util.MediaUtil;
+import com.android.messaging.util.OsUtil;
+import com.android.messaging.util.PhoneUtils;
+
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+
+public class FakeFactory extends Factory {
+    private Context mContext;
+    private FakeContext mFakeContext;
+    private BugleGservices mBugleGservices;
+    private BuglePrefs mBuglePrefs;
+    private DataModel mDataModel;
+    private UIIntents mUIIntents;
+    private MemoryCacheManager mMemoryCacheManager;
+    private MediaResourceManager mMediaResourceManager;
+    private MediaCacheManager mMediaCacheManager;
+    @Mock protected PhoneUtils mPhoneUtils;
+    private MediaUtil mMediaUtil;
+    private BugleCarrierConfigValuesLoader mCarrierConfigValuesLoader;
+
+    private FakeFactory() {
+    }
+
+    public static FakeFactory registerWithFakeContext(final Context context,
+            final FakeContext fake) {
+        // In tests we currently NEVER run the application/factory initialization
+        Assert.isTrue(!sRegistered);
+        Assert.isTrue(!sInitialized);
+
+        final FakeFactory factory = new FakeFactory();
+        Factory.setInstance(factory);
+
+        // At this point Factory is published. Services can now get initialized and depend on
+        // Factory.get().
+        factory.mContext = context;
+        factory.mFakeContext = fake;
+        factory.mMediaResourceManager = Mockito.mock(MediaResourceManager.class);
+        factory.mBugleGservices = new FakeBugleGservices();
+        factory.mBuglePrefs = new FakeBuglePrefs();
+        factory.mPhoneUtils = Mockito.mock(PhoneUtils.class);
+
+        ApnDatabase.initializeAppContext(context);
+
+        Mockito.when(factory.mPhoneUtils.getCanonicalBySystemLocale(Matchers.anyString()))
+                .thenAnswer(new Answer<String>() {
+                        @Override
+                        public String answer(final InvocationOnMock invocation) throws Throwable {
+                            final Object[] args = invocation.getArguments();
+                            return (String) args[0];
+                        }
+                    }
+                );
+        Mockito.when(factory.mPhoneUtils.getCanonicalBySimLocale(Matchers.anyString())).thenAnswer(
+                new Answer<String>() {
+                    @Override
+                    public String answer(final InvocationOnMock invocation) throws Throwable {
+                        final Object[] args = invocation.getArguments();
+                        return (String) args[0];
+                    }
+                }
+        );
+        Mockito.when(factory.mPhoneUtils.formatForDisplay(Matchers.anyString())).thenAnswer(
+                new Answer<String>() {
+                    @Override
+                    public String answer(final InvocationOnMock invocation) throws Throwable {
+                        return (String) invocation.getArguments()[0];
+                    }
+                }
+        );
+        if (OsUtil.isAtLeastL_MR1()) {
+            Mockito.when(factory.mPhoneUtils.toLMr1()).thenReturn(
+                    new PhoneUtils.LMr1() {
+                        @Override
+                        public SubscriptionInfo getActiveSubscriptionInfo() {
+                            return null;
+                        }
+
+                        @Override
+                        public List<SubscriptionInfo> getActiveSubscriptionInfoList() {
+                            return null;
+                        }
+
+                        @Override
+                        public void registerOnSubscriptionsChangedListener(
+                                final SubscriptionManager.OnSubscriptionsChangedListener listener) {
+                        }
+                    }
+            );
+        }
+        // By default only allow reading of system settings (that we provide) - can delegate
+        // to real provider if required.
+        final FakeContentProvider settings = new FakeContentProvider(context,
+                Settings.System.CONTENT_URI, false);
+        settings.addOverrideData(Settings.System.CONTENT_URI, "name=?", "time_12_24",
+                new String[] { "value" }, new Object[][] { { "12" } });
+        settings.addOverrideData(Settings.System.CONTENT_URI, "name=?", "sound_effects_enabled",
+                new String[] { "value" }, new Object[][] { { 1 } });
+
+        factory.withProvider(Settings.System.CONTENT_URI, settings);
+
+        return factory;
+    }
+
+    public static FakeFactory register(final Context applicationContext) {
+        final FakeContext context = new FakeContext(applicationContext);
+        return registerWithFakeContext(applicationContext, context);
+    }
+
+    public static FakeFactory registerWithoutFakeContext(final Context applicationContext) {
+        return registerWithFakeContext(applicationContext, null);
+    }
+
+    @Override
+    public void onRequiredPermissionsAcquired() {
+    }
+
+    @Override
+    public Context getApplicationContext() {
+        return ((mFakeContext != null) ? mFakeContext : mContext );
+    }
+
+    @Override
+    public DataModel getDataModel() {
+        return mDataModel;
+    }
+
+    @Override
+    public BugleGservices getBugleGservices() {
+        return mBugleGservices;
+    }
+
+    @Override
+    public BuglePrefs getApplicationPrefs() {
+        return mBuglePrefs;
+    }
+
+    @Override
+    public BuglePrefs getWidgetPrefs() {
+        return mBuglePrefs;
+    }
+
+    @Override
+    public BuglePrefs getSubscriptionPrefs(final int subId) {
+        return mBuglePrefs;
+    }
+
+    @Override
+    public UIIntents getUIIntents() {
+        return mUIIntents;
+    }
+
+    @Override
+    public MemoryCacheManager getMemoryCacheManager() {
+        return mMemoryCacheManager;
+    }
+
+    @Override
+    public MediaResourceManager getMediaResourceManager() {
+        return mMediaResourceManager;
+    }
+
+    @Override
+    public MediaCacheManager getMediaCacheManager() {
+        return mMediaCacheManager;
+    }
+
+    @Override
+    public PhoneUtils getPhoneUtils(final int subId) {
+        return mPhoneUtils;
+    }
+
+    @Override
+    public MediaUtil getMediaUtil() {
+        return mMediaUtil;
+    }
+
+    @Override
+    public BugleCarrierConfigValuesLoader getCarrierConfigValuesLoader() {
+        return mCarrierConfigValuesLoader;
+    }
+
+    @Override
+    public ContactContentObserver getContactContentObserver() {
+        return null;
+    }
+
+    @Override
+    public void reclaimMemory() {
+    }
+
+    @Override
+    public void onActivityResume() {
+    }
+
+    public FakeFactory withDataModel(final DataModel dataModel) {
+        this.mDataModel = dataModel;
+        return this;
+    }
+
+    public FakeFactory withUIIntents(final UIIntents uiIntents) {
+        this.mUIIntents = uiIntents;
+        return this;
+    }
+
+    public FakeFactory withMemoryCacheManager(final MemoryCacheManager memoryCacheManager) {
+        this.mMemoryCacheManager = memoryCacheManager;
+        return this;
+    }
+
+    public FakeFactory withBugleGservices(final BugleGservices bugleGservices) {
+        this.mBugleGservices = bugleGservices;
+        return this;
+    }
+
+    public FakeFactory withMediaCacheManager(final MediaCacheManager mediaCacheManager) {
+        this.mMediaCacheManager = mediaCacheManager;
+        return this;
+    }
+
+    public FakeFactory withProvider(final Uri uri, final ContentProvider provider) {
+        if (mFakeContext != null) {
+            mFakeContext.addContentProvider(uri.getAuthority(), provider);
+        }
+        return this;
+    }
+
+    public FakeFactory withDefaultProvider(final Uri uri) {
+        if (mFakeContext != null) {
+            mFakeContext.addDefaultProvider(this.mContext, uri);
+        }
+        return this;
+    }
+
+    public FakeFactory withMediaUtil(final MediaUtil mediaUtil) {
+        this.mMediaUtil = mediaUtil;
+        return this;
+    }
+
+    public FakeFactory withCarrierConfigValuesLoader(
+            final BugleCarrierConfigValuesLoader carrierConfigValuesLoader) {
+        this.mCarrierConfigValuesLoader = carrierConfigValuesLoader;
+        return this;
+    }
+}
diff --git a/tests/src/com/android/messaging/TestUtil.java b/tests/src/com/android/messaging/TestUtil.java
new file mode 100644
index 0000000..77a8450
--- /dev/null
+++ b/tests/src/com/android/messaging/TestUtil.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 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.messaging;
+
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.PowerManager;
+import android.test.InstrumentationTestCase;
+
+import com.android.messaging.util.LogUtil;
+import com.android.messaging.widget.BugleWidgetProvider;
+import com.android.messaging.widget.WidgetConversationProvider;
+
+import junit.framework.TestCase;
+
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Helpers that can be called from all test base classes to 'reset' state and prevent as much as
+ * possible having side effects leak from one test to another.
+ */
+public class TestUtil {
+    public static void testSetup(final Context context, final TestCase testCase) {
+        haltIfTestsAreNotAbleToRun(context);
+
+        // Workaround to get mockito to work.
+        // See https://code.google.com/p/dexmaker/issues/detail?id=2. TODO: Apparently
+        // solvable by using a different runner.
+        System.setProperty("dexmaker.dexcache",
+                context.getCacheDir().getPath());
+
+        // Initialize @Mock objects.
+        MockitoAnnotations.initMocks(testCase);
+
+        // Tests have to explicitly override this
+        Factory.setInstance(null);
+    }
+
+    public static void testTeardown(final TestCase testCase) {
+        if (testCase instanceof InstrumentationTestCase) {
+            // Make sure the test case is finished running or we'll get NPEs when accessing
+            // Fragment.get()
+            ((InstrumentationTestCase) testCase).getInstrumentation().waitForIdleSync();
+        }
+        Factory.setInstance(null);
+    }
+
+    private static void haltIfTestsAreNotAbleToRun(final Context context) {
+        final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        if (!pm.isScreenOn()) {
+            // Ideally we could turn it on for you using the WindowManager, but we currently run
+            // the tests independently of the activity life cycle.
+            LogUtil.wtf(LogUtil.BUGLE_TAG, "You need to turn on your screen to run tests!");
+        }
+
+        final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+        int [] conversationWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+                WidgetConversationProvider.class));
+        int [] conversationListWidgetIds = appWidgetManager.getAppWidgetIds(new
+                ComponentName(context, BugleWidgetProvider.class));
+
+        if ((conversationWidgetIds.length > 0) || (conversationListWidgetIds.length > 0)) {
+            // Currently widgets asynchronously access our content providers and singletons which
+            // interacts badly with our test setup and tear down.
+            LogUtil.wtf(LogUtil.BUGLE_TAG, "You currently can't reliably run unit tests" +
+                    " with a Messaging widget on your desktop!");
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/BindingTest.java b/tests/src/com/android/messaging/datamodel/BindingTest.java
new file mode 100644
index 0000000..9205657
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/BindingTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.datamodel.binding.BindableData;
+import com.android.messaging.datamodel.binding.Binding;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.datamodel.binding.ImmutableBindingRef;
+
+/**
+ * Test binding
+ */
+@SmallTest
+public class BindingTest extends BugleTestCase {
+    private static final Object TEST_DATA_ID = "myDataId";
+    private static final Object YOUR_DATA_ID = "yourDataId";
+
+    public void testBindingStartsUnbound() {
+        final Binding<TestBindableData> binding = BindingBase.createBinding(this);
+        assertNull(binding.getBindingId());
+    }
+
+    public void testDataStartsUnbound() {
+        final TestBindableData data = new TestBindableData(TEST_DATA_ID);
+        assertFalse(data.isBound());
+    }
+
+    public void testBindingUpdatesDataAndBindee() {
+        final Binding<TestBindableData> binding = BindingBase.createBinding(this);
+        final TestBindableData data = new TestBindableData(TEST_DATA_ID);
+        binding.bind(data);
+        assertTrue(binding.isBound());
+        assertEquals(binding.getData(), data);
+        assertTrue(data.isBound(binding.getBindingId()));
+        assertFalse(data.isBound("SomeRandomString"));
+        assertNotNull(binding.getBindingId());
+        assertFalse(data.mListenersUnregistered);
+    }
+
+    public void testRebindingFails() {
+        final Binding<TestBindableData> binding = BindingBase.createBinding(this);
+        final TestBindableData yours = new TestBindableData(YOUR_DATA_ID);
+        binding.bind(yours);
+        assertEquals(binding.getData(), yours);
+        assertTrue(yours.isBound(binding.getBindingId()));
+        final TestBindableData data = new TestBindableData(TEST_DATA_ID);
+        try {
+            binding.bind(data);
+            fail();
+        } catch (final IllegalStateException e) {
+        }
+        assertTrue(binding.isBound());
+        assertEquals(binding.getData(), yours);
+        assertTrue(yours.isBound(binding.getBindingId()));
+    }
+
+    public void testUnbindingClearsDataAndBindee() {
+        final Binding<TestBindableData> binding = BindingBase.createBinding(this);
+        final TestBindableData data = new TestBindableData(TEST_DATA_ID);
+        binding.bind(data);
+        assertTrue(data.isBound(binding.getBindingId()));
+        assertTrue(binding.isBound());
+        binding.unbind();
+        try {
+            final TestBindableData other = binding.getData();
+            fail();
+        } catch (final IllegalStateException e) {
+        }
+        assertFalse(data.isBound());
+        assertNull(binding.getBindingId());
+        assertTrue(data.mListenersUnregistered);
+    }
+
+    public void testUnbindingAndRebinding() {
+        final Binding<TestBindableData> binding = BindingBase.createBinding(this);
+        final TestBindableData yours = new TestBindableData(YOUR_DATA_ID);
+        binding.bind(yours);
+        assertEquals(binding.getData(), yours);
+        assertTrue(yours.isBound(binding.getBindingId()));
+        binding.unbind();
+        assertFalse(yours.isBound());
+        assertNull(binding.getBindingId());
+
+        final TestBindableData data = new TestBindableData(TEST_DATA_ID);
+        binding.bind(data);
+        assertEquals(binding.getData(), data);
+        assertTrue(data.isBound(binding.getBindingId()));
+        assertFalse(data.isBound("SomeRandomString"));
+        assertTrue(binding.isBound());
+        assertNotNull(binding.getBindingId());
+    }
+
+    public void testBindingReference() {
+        final Binding<TestBindableData> binding = BindingBase.createBinding(this);
+        final TestBindableData data = new TestBindableData(TEST_DATA_ID);
+        binding.bind(data);
+        assertEquals(binding.getData(), data);
+        assertTrue(data.isBound(binding.getBindingId()));
+
+        final ImmutableBindingRef<TestBindableData> bindingRef =
+                BindingBase.createBindingReference(binding);
+        assertEquals(bindingRef.getData(), data);
+        assertTrue(data.isBound(bindingRef.getBindingId()));
+
+        binding.unbind();
+        assertFalse(binding.isBound());
+        assertNull(binding.getBindingId());
+        assertFalse(bindingRef.isBound());
+        assertNull(bindingRef.getBindingId());
+    }
+
+    static class TestBindableData extends BindableData {
+        private final Object mDataId;
+        public boolean mListenersUnregistered;
+
+        public TestBindableData(final Object dataId) {
+            mDataId = dataId;
+            mListenersUnregistered = false;
+        }
+
+        @Override
+        public void unregisterListeners() {
+            mListenersUnregistered = true;
+        }
+
+        @Override
+        public boolean isBound() {
+            return super.isBound();
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/BitmapPoolTest.java b/tests/src/com/android/messaging/datamodel/BitmapPoolTest.java
new file mode 100644
index 0000000..6d0aaa3
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/BitmapPoolTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+
+import org.mockito.Mock;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@SmallTest
+public class BitmapPoolTest extends BugleTestCase {
+    private static final int POOL_SIZE = 5;
+    private static final int IMAGE_DIM = 1;
+    private static final String NAME = "BitmapPoolTest";
+
+    @Mock private MemoryCacheManager mockMemoryCacheManager;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getTestContext())
+                .withMemoryCacheManager(mockMemoryCacheManager);
+    }
+
+    private Set<Bitmap> fillPoolAndGetPoolContents(final BitmapPool pool, final int width,
+            final int height) {
+        final Set<Bitmap> returnedBitmaps = new HashSet<Bitmap>();
+        for (int i = 0; i < POOL_SIZE; i++) {
+            final Bitmap temp = pool.createOrReuseBitmap(width, height);
+            assertFalse(returnedBitmaps.contains(temp));
+            returnedBitmaps.add(temp);
+        }
+        for (final Bitmap b : returnedBitmaps) {
+            pool.reclaimBitmap(b);
+        }
+        assertTrue(pool.isFull(width, height));
+        return returnedBitmaps;
+    }
+
+    public void testCreateAndPutBackInPoolTest() {
+        final BitmapPool pool = new BitmapPool(POOL_SIZE, NAME);
+        final Bitmap bitmap = pool.createOrReuseBitmap(IMAGE_DIM, IMAGE_DIM);
+        assertFalse(bitmap.isRecycled());
+        assertFalse(pool.isFull(IMAGE_DIM, IMAGE_DIM));
+        pool.reclaimBitmap(bitmap);
+
+        // Don't recycle because the pool isn't full yet.
+        assertFalse(bitmap.isRecycled());
+    }
+
+    public void testCreateBeyondFullAndCheckReuseTest() {
+        final BitmapPool pool = new BitmapPool(POOL_SIZE, NAME);
+        final Set<Bitmap> returnedBitmaps =
+                fillPoolAndGetPoolContents(pool, IMAGE_DIM, IMAGE_DIM);
+        final Bitmap overflowBitmap = pool.createOrReuseBitmap(IMAGE_DIM, IMAGE_DIM);
+        assertFalse(overflowBitmap.isRecycled());
+        assertTrue(returnedBitmaps.contains(overflowBitmap));
+    }
+
+    /**
+     * Make sure that we have the correct options to create mutable for bitmap pool reuse.
+     */
+    public void testAssertBitmapOptionsAreMutable() {
+        final BitmapFactory.Options options =
+                BitmapPool.getBitmapOptionsForPool(false, IMAGE_DIM, IMAGE_DIM);
+        assertTrue(options.inMutable);
+    }
+
+    public void testDecodeFromResourceBitmap() {
+        final BitmapPool pool = new BitmapPool(POOL_SIZE, NAME);
+        final BitmapFactory.Options options =
+                BitmapPool.getBitmapOptionsForPool(true, IMAGE_DIM, IMAGE_DIM);
+        final Resources resources = getContext().getResources();
+        final Bitmap resourceBitmap = pool.decodeSampledBitmapFromResource(
+                R.drawable.msg_bubble_incoming, resources, options, IMAGE_DIM, IMAGE_DIM);
+        assertNotNull(resourceBitmap);
+        assertTrue(resourceBitmap.getByteCount() > 0);
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/BugleServiceTestCase.java b/tests/src/com/android/messaging/datamodel/BugleServiceTestCase.java
new file mode 100644
index 0000000..42eb647
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/BugleServiceTestCase.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.app.Service;
+import android.test.ServiceTestCase;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.TestUtil;
+
+
+/*
+ * Base class for service tests that takes care of housekeeping that is commong amongst our service
+ * test case.
+ */
+public abstract class BugleServiceTestCase<T extends Service> extends ServiceTestCase<T> {
+
+    static {
+        // Set flag during loading of test cases to prevent application initialization starting
+        BugleTestCase.setTestsRunning();
+    }
+
+    public BugleServiceTestCase(final Class<T> serviceClass) {
+        super(serviceClass);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+      super.setUp();
+      TestUtil.testSetup(getContext(), this);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        TestUtil.testTeardown(this);
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/messaging/datamodel/ConversationListTest.java b/tests/src/com/android/messaging/datamodel/ConversationListTest.java
new file mode 100644
index 0000000..f45696b
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/ConversationListTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+
+@SmallTest
+public class ConversationListTest extends BugleTestCase {
+    public void testTesting() {
+        assertTrue(true);
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/DataModelTest.java b/tests/src/com/android/messaging/datamodel/DataModelTest.java
new file mode 100644
index 0000000..71723a4
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/DataModelTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.data.ConversationData;
+import com.android.messaging.datamodel.data.ConversationListData;
+import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
+import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
+
+import org.mockito.Mock;
+
+public class DataModelTest extends BugleTestCase {
+
+    DataModel dataModel;
+    @Mock protected ConversationDataListener mockConversationDataListener;
+    @Mock protected ConversationListDataListener mockConversationListDataListener;
+
+    @Override
+    protected void setUp() throws Exception {
+      super.setUp();
+      dataModel = new DataModelImpl(getTestContext());
+      FakeFactory.register(mContext)
+              .withDataModel(dataModel);
+    }
+
+    @SmallTest
+    public void testCreateConversationList() {
+        final ConversationListData list = dataModel.createConversationListData(getContext(),
+                        mockConversationListDataListener, true);
+        assertTrue(list instanceof ConversationListData);
+        final ConversationData conv = dataModel.createConversationData(getContext(),
+                mockConversationDataListener, "testConversation");
+        assertTrue(conv instanceof ConversationData);
+    }
+
+    private static final String FOCUSED_CONV_ID = "focused_conv_id";
+
+    @SmallTest
+    public void testFocusedConversationIsObservable() {
+        dataModel.setFocusedConversation(FOCUSED_CONV_ID);
+        assertTrue(dataModel.isNewMessageObservable(FOCUSED_CONV_ID));
+        dataModel.setFocusedConversation(null);
+        assertFalse(dataModel.isNewMessageObservable(FOCUSED_CONV_ID));
+    }
+
+    @SmallTest
+    public void testConversationIsObservableInList() {
+        dataModel.setConversationListScrolledToNewestConversation(true);
+        assertTrue(dataModel.isNewMessageObservable(FOCUSED_CONV_ID));
+        dataModel.setConversationListScrolledToNewestConversation(false);
+        assertFalse(dataModel.isNewMessageObservable(FOCUSED_CONV_ID));
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/FakeCursor.java b/tests/src/com/android/messaging/datamodel/FakeCursor.java
new file mode 100644
index 0000000..59d1b89
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/FakeCursor.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.os.Bundle;
+import android.test.mock.MockCursor;
+
+import java.util.ArrayList;
+
+/**
+ * A simple in memory fake cursor that can be used for UI tests.
+ */
+public class FakeCursor extends MockCursor {
+    private final ArrayList<Integer> mProjection;
+    private final String[] mColumnNamesOfData;
+    private final Object[][] mData;
+    private int mIndex;
+
+    public FakeCursor(final String[] projection, final String[] columnNames,
+            final Object[][] data) {
+        mColumnNamesOfData = columnNames;
+        mData = data;
+        mIndex = -1;
+        mProjection = new ArrayList<Integer>(projection.length);
+        for (final String column : projection) {
+            mProjection.add(getColumnIndex(column));
+        }
+    }
+
+    public Object getAt(final String columnName, final int row) {
+        final int dataIdx = getColumnIndex(columnName);
+        return (dataIdx < 0 || row < 0 || row >= mData.length) ? 0 : mData[row][dataIdx];
+    }
+
+    @Override
+    public int getCount() {
+        return mData.length;
+    }
+
+    @Override
+    public boolean isFirst() {
+        return mIndex == 0;
+    }
+
+    @Override
+    public boolean isLast() {
+        return mIndex == mData.length - 1;
+    }
+
+    @Override
+    public boolean moveToFirst() {
+        if (mData.length == 0) {
+            return false;
+        }
+        mIndex = 0;
+        return true;
+    }
+
+    @Override
+    public boolean moveToPosition(final int position) {
+        if (position < 0 || position >= mData.length) {
+            return false;
+        }
+        mIndex = position;
+        return true;
+    }
+
+    @Override
+    public int getPosition() {
+        return mIndex;
+    }
+
+    @Override
+    public boolean moveToPrevious() {
+        if (mIndex <= 0) {
+            return false;
+        }
+        mIndex--;
+        return true;
+    }
+
+    @Override
+    public boolean moveToNext() {
+        if (mIndex == mData.length - 1) {
+            return false;
+        }
+
+        mIndex++;
+        return true;
+    }
+
+    @Override
+    public int getColumnCount() {
+        return mColumnNamesOfData.length;
+    }
+
+    @Override
+    public int getColumnIndex(final String columnName) {
+        for (int i = 0 ; i < mColumnNamesOfData.length ; i++) {
+            if (mColumnNamesOfData[i].equals(columnName)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    @Override
+    public int getColumnIndexOrThrow(final String columnName) {
+        final int result = getColumnIndex(columnName);
+        if (result == -1) {
+            throw new IllegalArgumentException();
+        }
+
+        return result;
+    }
+
+    @Override
+    public String getString(final int columnIndex) {
+        final int dataIdx = mProjection.get(columnIndex);
+        final Object obj = (dataIdx < 0 ? null : mData[mIndex][dataIdx]);
+        return (obj == null ? null : obj.toString());
+    }
+
+    @Override
+    public int getInt(final int columnIndex) {
+        final int dataIdx = mProjection.get(columnIndex);
+        return (dataIdx < 0 ? 0 : (Integer) mData[mIndex][dataIdx]);
+    }
+
+    @Override
+    public long getLong(final int columnIndex) {
+        final int dataIdx = mProjection.get(columnIndex);
+        return (dataIdx < 0 ? 0 : (Long) mData[mIndex][dataIdx]);
+    }
+
+    @Override
+    public void close() {
+    }
+
+    @Override
+    public boolean isClosed() {
+        return false;
+    }
+
+    @Override
+    public Bundle getExtras() { return null; }
+}
diff --git a/tests/src/com/android/messaging/datamodel/FakeDataModel.java b/tests/src/com/android/messaging/datamodel/FakeDataModel.java
new file mode 100644
index 0000000..5e80eab
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/FakeDataModel.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.test.RenamingDelegatingContext;
+
+import com.android.messaging.datamodel.action.ActionService;
+import com.android.messaging.datamodel.action.BackgroundWorker;
+import com.android.messaging.datamodel.data.BlockedParticipantsData;
+import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
+import com.android.messaging.datamodel.data.ContactListItemData;
+import com.android.messaging.datamodel.data.ContactPickerData;
+import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
+import com.android.messaging.datamodel.data.ConversationData;
+import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
+import com.android.messaging.datamodel.data.ConversationListData;
+import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
+import com.android.messaging.datamodel.data.DraftMessageData;
+import com.android.messaging.datamodel.data.GalleryGridItemData;
+import com.android.messaging.datamodel.data.LaunchConversationData;
+import com.android.messaging.datamodel.data.LaunchConversationData.LaunchConversationDataListener;
+import com.android.messaging.datamodel.data.MediaPickerData;
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.datamodel.data.ParticipantListItemData;
+import com.android.messaging.datamodel.data.PeopleAndOptionsData;
+import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
+import com.android.messaging.datamodel.data.PeopleOptionsItemData;
+import com.android.messaging.datamodel.data.SettingsData;
+import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
+import com.android.messaging.datamodel.data.SubscriptionListData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.datamodel.data.VCardContactItemData;
+import com.android.messaging.util.ConnectivityUtil;
+
+public class FakeDataModel extends DataModel {
+    private BackgroundWorker mWorker;
+    private ActionService mActionService;
+    private final DatabaseHelper mDatabaseHelper;
+    private ConversationListData mConversationListData;
+    private ContactPickerData mContactPickerData;
+    private MediaPickerData mMediaPickerData;
+    private PeopleAndOptionsData mPeopleAndOptionsData;
+    private ConnectivityUtil mConnectivityUtil;
+    private SyncManager mSyncManager;
+    private SettingsData mSettingsData;
+    private DraftMessageData mDraftMessageData;
+
+    public FakeDataModel(final Context context) {
+        super();
+        if (context instanceof RenamingDelegatingContext) {
+            mDatabaseHelper = DatabaseHelper.getNewInstanceForTest(context);
+        } else {
+            mDatabaseHelper = null;
+        }
+    }
+
+    @Override
+    public BackgroundWorker getBackgroundWorkerForActionService() {
+        return mWorker;
+    }
+
+    public FakeDataModel withBackgroundWorkerForActionService(final BackgroundWorker worker) {
+        mWorker = worker;
+        return this;
+    }
+
+    public FakeDataModel withActionService(final ActionService ActionService) {
+        mActionService = ActionService;
+        return this;
+    }
+
+    public FakeDataModel withConversationListData(final ConversationListData conversationListData) {
+        mConversationListData = conversationListData;
+        return this;
+    }
+
+    public FakeDataModel withContactPickerData(final ContactPickerData contactPickerData) {
+        mContactPickerData = contactPickerData;
+        return this;
+    }
+
+    public FakeDataModel withMediaPickerData(final MediaPickerData mediaPickerData) {
+        mMediaPickerData = mediaPickerData;
+        return this;
+    }
+
+    public FakeDataModel withConnectivityUtil(final ConnectivityUtil connectivityUtil) {
+        mConnectivityUtil = connectivityUtil;
+        return this;
+    }
+
+    public FakeDataModel withSyncManager(final SyncManager syncManager) {
+        mSyncManager = syncManager;
+        return this;
+    }
+
+    public FakeDataModel withPeopleAndOptionsData(final PeopleAndOptionsData peopleAndOptionsData) {
+        mPeopleAndOptionsData = peopleAndOptionsData;
+        return this;
+    }
+
+    public FakeDataModel withSettingsData(final SettingsData settingsData) {
+        mSettingsData = settingsData;
+        return this;
+    }
+
+    public FakeDataModel withDraftMessageData(final DraftMessageData draftMessageData) {
+        mDraftMessageData = draftMessageData;
+        return this;
+    }
+
+    @Override
+    public ConversationListData createConversationListData(final Context context,
+            final ConversationListDataListener listener, final boolean archivedMode) {
+        return mConversationListData;
+    }
+
+    @Override
+    public ConversationData createConversationData(final Context context,
+            final ConversationDataListener listener, final String conversationId) {
+        throw new IllegalStateException("Add withXXX or mock this method");
+    }
+
+    @Override
+    public ContactListItemData createContactListItemData() {
+        // This is a lightweight data holder object for each individual list item for which
+        // we don't perform any data request, so we can directly return a new instance.
+        return new ContactListItemData();
+    }
+
+    @Override
+    public ContactPickerData createContactPickerData(final Context context,
+            final ContactPickerDataListener listener) {
+        return mContactPickerData;
+    }
+
+    @Override
+    public MediaPickerData createMediaPickerData(final Context context) {
+        return mMediaPickerData;
+    }
+
+    @Override
+    public GalleryGridItemData createGalleryGridItemData() {
+        // This is a lightweight data holder object for each individual grid item for which
+        // we don't perform any data request, so we can directly return a new instance.
+        return new GalleryGridItemData();
+    }
+
+    @Override
+    public LaunchConversationData createLaunchConversationData(
+            final LaunchConversationDataListener listener) {
+       return new LaunchConversationData(listener);
+    }
+
+    @Override
+    public PeopleOptionsItemData createPeopleOptionsItemData(final Context context) {
+        return new PeopleOptionsItemData(context);
+    }
+
+    @Override
+    public PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
+            final Context context, final PeopleAndOptionsDataListener listener) {
+        return mPeopleAndOptionsData;
+    }
+
+    @Override
+    public VCardContactItemData createVCardContactItemData(final Context context,
+            final MessagePartData data) {
+        return new VCardContactItemData(context, data);
+    }
+
+    @Override
+    public VCardContactItemData createVCardContactItemData(final Context context,
+            final Uri vCardUri) {
+        return new VCardContactItemData(context, vCardUri);
+    }
+
+    @Override
+    public ParticipantListItemData createParticipantListItemData(
+            final ParticipantData participant) {
+        return new ParticipantListItemData(participant);
+    }
+
+    @Override
+    public SubscriptionListData createSubscriptonListData(Context context) {
+        return new SubscriptionListData(context);
+    }
+
+    @Override
+    public SettingsData createSettingsData(Context context, SettingsDataListener listener) {
+        return mSettingsData;
+    }
+
+    @Override
+    public DraftMessageData createDraftMessageData(String conversationId) {
+        return mDraftMessageData;
+    }
+
+    @Override
+    public ActionService getActionService() {
+        return mActionService;
+    }
+
+    @Override
+    public ConnectivityUtil getConnectivityUtil() {
+        return mConnectivityUtil;
+    }
+
+    @Override
+    public SyncManager getSyncManager() {
+        return mSyncManager;
+    }
+
+    @Override
+    public DatabaseWrapper getDatabase() {
+        // Note this will crash unless the application context is redirected...
+        // This is by design so that tests do not inadvertently use the real database
+        return mDatabaseHelper.getDatabase();
+    }
+
+    @Override
+    void onCreateTables(final SQLiteDatabase db) {
+        TestDataFactory.createTestData(db);
+    }
+
+    @Override
+    public void onActivityResume() {
+    }
+
+    @Override
+    public void onApplicationCreated() {
+    }
+
+    @Override
+    public BlockedParticipantsData createBlockedParticipantsData(Context context,
+            BlockedParticipantsDataListener listener) {
+        return new BlockedParticipantsData(context, listener);
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/FrequentContactsCursorBuilderTest.java b/tests/src/com/android/messaging/datamodel/FrequentContactsCursorBuilderTest.java
new file mode 100644
index 0000000..6b78a07
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/FrequentContactsCursorBuilderTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.database.Cursor;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.util.ContactUtil;
+
+@SmallTest
+public class FrequentContactsCursorBuilderTest extends BugleTestCase {
+
+    private void verifyBuiltCursor(final Cursor expected, final Cursor actual) {
+        final int rowCount = expected.getCount();
+        final int columnCount = expected.getColumnCount();
+        assertEquals(rowCount, actual.getCount());
+        assertEquals(columnCount, actual.getColumnCount());
+        for (int i = 0; i < rowCount; i++) {
+            expected.moveToPosition(i);
+            actual.moveToPosition(i);
+            assertEquals(expected.getLong(ContactUtil.INDEX_DATA_ID),
+                    actual.getLong(ContactUtil.INDEX_DATA_ID));
+            assertEquals(expected.getLong(ContactUtil.INDEX_CONTACT_ID),
+                    actual.getLong(ContactUtil.INDEX_CONTACT_ID));
+            assertEquals(expected.getString(ContactUtil.INDEX_LOOKUP_KEY),
+                    actual.getString(ContactUtil.INDEX_LOOKUP_KEY));
+            assertEquals(expected.getString(ContactUtil.INDEX_DISPLAY_NAME),
+                    actual.getString(ContactUtil.INDEX_DISPLAY_NAME));
+            assertEquals(expected.getString(ContactUtil.INDEX_PHOTO_URI),
+                    actual.getString(ContactUtil.INDEX_PHOTO_URI));
+            assertEquals(expected.getString(ContactUtil.INDEX_PHONE_EMAIL),
+                    actual.getString(ContactUtil.INDEX_PHONE_EMAIL));
+            assertEquals(expected.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE),
+                    actual.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE));
+            assertEquals(expected.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL),
+                    actual.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL));
+        }
+    }
+
+    public void testIncompleteBuild() {
+        final FrequentContactsCursorBuilder builder = new FrequentContactsCursorBuilder();
+        assertNull(builder.build());
+        assertNull(builder.setFrequents(TestDataFactory.getStrequentContactsCursor()).build());
+        builder.resetBuilder();
+        assertNull(builder.build());
+        assertNull(builder.setAllContacts(TestDataFactory.getAllContactListCursor()).build());
+    }
+
+    public void testBuildOnce() {
+        final Cursor cursor = new FrequentContactsCursorBuilder()
+            .setAllContacts(TestDataFactory.getAllContactListCursor())
+            .setFrequents(TestDataFactory.getStrequentContactsCursor())
+            .build();
+        assertNotNull(cursor);
+        verifyBuiltCursor(TestDataFactory.getFrequentContactListCursor(), cursor);
+    }
+
+    public void testBuildTwice() {
+        final FrequentContactsCursorBuilder builder = new FrequentContactsCursorBuilder();
+        final Cursor firstCursor = builder
+            .setAllContacts(TestDataFactory.getAllContactListCursor())
+            .setFrequents(TestDataFactory.getStrequentContactsCursor())
+            .build();
+        assertNotNull(firstCursor);
+        builder.resetBuilder();
+        assertNull(builder.build());
+
+        final Cursor secondCursor = builder
+                .setAllContacts(TestDataFactory.getAllContactListCursor())
+                .setFrequents(TestDataFactory.getStrequentContactsCursor())
+                .build();
+        assertNotNull(firstCursor);
+        verifyBuiltCursor(TestDataFactory.getFrequentContactListCursor(), secondCursor);
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/MemoryCacheManagerTest.java b/tests/src/com/android/messaging/datamodel/MemoryCacheManagerTest.java
new file mode 100644
index 0000000..5da9e27
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/MemoryCacheManagerTest.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.datamodel.MemoryCacheManager.MemoryCache;
+
+import org.mockito.Mockito;
+
+@SmallTest
+public class MemoryCacheManagerTest extends AndroidTestCase {
+
+    public void testRegisterCachesGetReclaimed() {
+        final MemoryCache mockMemoryCache = Mockito.mock(MemoryCache.class);
+        final MemoryCache otherMockMemoryCache = Mockito.mock(MemoryCache.class);
+        final MemoryCacheManager memoryCacheManager = new MemoryCacheManager();
+
+        memoryCacheManager.registerMemoryCache(mockMemoryCache);
+        memoryCacheManager.registerMemoryCache(otherMockMemoryCache);
+        memoryCacheManager.reclaimMemory();
+        memoryCacheManager.unregisterMemoryCache(otherMockMemoryCache);
+        memoryCacheManager.reclaimMemory();
+
+        Mockito.verify(mockMemoryCache, Mockito.times(2)).reclaim();
+        Mockito.verify(otherMockMemoryCache, Mockito.times(1)).reclaim();
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/ParticipantRefreshTest.java b/tests/src/com/android/messaging/datamodel/ParticipantRefreshTest.java
new file mode 100644
index 0000000..cd1d6c7
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/ParticipantRefreshTest.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeContentProvider;
+import com.android.messaging.FakeContext;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.datamodel.data.ParticipantData.ParticipantsQuery;
+import com.android.messaging.util.ContactUtil;
+
+import org.junit.Assert;
+
+/**
+ * Utility class for testing ParticipantRefresh class for different scenarios.
+ */
+@SmallTest
+public class ParticipantRefreshTest extends BugleTestCase {
+    private FakeContext mContext;
+    FakeFactory mFakeFactory;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mContext = new FakeContext(getTestContext());
+
+        final ContentProvider provider = new MessagingContentProvider();
+        provider.attachInfo(mContext, null);
+        mContext.addContentProvider(MessagingContentProvider.AUTHORITY, provider);
+
+        final FakeDataModel fakeDataModel = new FakeDataModel(mContext);
+        mFakeFactory = FakeFactory.registerWithFakeContext(getTestContext(), mContext)
+                .withDataModel(fakeDataModel);
+    }
+
+    /**
+     * Add some phonelookup result into take PhoneLookup content provider. This will be
+     * used for doing phone lookup during participant refresh.
+     */
+    private void addPhoneLookup(final String phone, final Object[][] lookupResult) {
+        final Uri uri = ContactUtil.lookupPhone(mContext, phone).getUri();
+        final FakeContentProvider phoneLookup = new FakeContentProvider(mContext,
+                uri, false);
+        phoneLookup.addOverrideData(uri, null, null, ContactUtil.PhoneLookupQuery.PROJECTION,
+                lookupResult);
+        mFakeFactory.withProvider(uri, phoneLookup);
+    }
+
+    /**
+     * Add some participant to test database.
+     */
+    private void addParticipant(final String normalizedDestination, final long contactId,
+            final String name, final String photoUrl) {
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+        final ContentValues values = new ContentValues();
+
+        values.put(ParticipantColumns.NORMALIZED_DESTINATION, normalizedDestination);
+        values.put(ParticipantColumns.CONTACT_ID, contactId);
+        values.put(ParticipantColumns.FULL_NAME, name);
+        values.put(ParticipantColumns.PROFILE_PHOTO_URI, photoUrl);
+
+        db.beginTransaction();
+        try {
+            db.insert(DatabaseHelper.PARTICIPANTS_TABLE, null, values);
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * Verify that participant in the database has expected contacdtId, name and photoUrl fields.
+     */
+    private void verifyParticipant(final String normalizedDestination, final long contactId,
+            final String name, final String photoUrl) {
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+        db.beginTransaction();
+        try {
+            final String selection = ParticipantColumns.NORMALIZED_DESTINATION + "=?";
+            final String[] selectionArgs = new String[] { normalizedDestination };
+
+            final Cursor cursor = db.query(DatabaseHelper.PARTICIPANTS_TABLE,
+                    ParticipantsQuery.PROJECTION, selection, selectionArgs, null, null, null);
+
+            if (cursor == null || cursor.getCount() != 1) {
+                Assert.fail("Should have participants for:" + normalizedDestination);
+                return;
+            }
+
+            cursor.moveToFirst();
+            final int currentContactId = cursor.getInt(ParticipantsQuery.INDEX_CONTACT_ID);
+            final String currentName = cursor.getString(ParticipantsQuery.INDEX_FULL_NAME);
+            final String currentPhotoUrl =
+                    cursor.getString(ParticipantsQuery.INDEX_PROFILE_PHOTO_URI);
+            if (currentContactId != contactId) {
+                Assert.fail("Contact Id doesn't match. normalizedNumber=" + normalizedDestination +
+                        " expected=" + contactId + " actual=" + currentContactId);
+                return;
+            }
+
+            if (!TextUtils.equals(currentName, name)) {
+                Assert.fail("Name doesn't match. normalizedNumber=" + normalizedDestination +
+                        " expected=" + name + " actual=" + currentName);
+                return;
+            }
+
+            if (!TextUtils.equals(currentPhotoUrl, photoUrl)) {
+                Assert.fail("Contact Id doesn't match. normalizedNumber=" + normalizedDestination +
+                        " expected=" + photoUrl + " actual=" + currentPhotoUrl);
+                return;
+            }
+
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * Verify that incremental refresh will resolve previously not resolved participants.
+     */
+    public void testIncrementalRefreshNotResolvedSingleMatch() {
+        addParticipant("650-123-1233", ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED,
+                null, null);
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_INCREMENTAL);
+        verifyParticipant("650-123-1233", 1, "John", "content://photo/john");
+    }
+
+    /**
+     * Verify that incremental refresh will resolve previously not resolved participants.
+     */
+    public void testIncrementalRefreshNotResolvedMultiMatch() {
+        addParticipant("650-123-1233", ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED,
+                null, null);
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null },
+                { 2L, "Joe", "content://photo/joe", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_INCREMENTAL);
+        verifyParticipant("650-123-1233", 1, "John", "content://photo/john");
+    }
+
+    /**
+     * Verify that incremental refresh will not touch already-resolved participants.
+     */
+    public void testIncrementalRefreshResolvedSingleMatch() {
+        addParticipant("650-123-1233", 1, "Joh", "content://photo/joh");
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_INCREMENTAL);
+        verifyParticipant("650-123-1233", 1, "Joh", "content://photo/joh");
+    }
+
+    /**
+     * Verify that full refresh will correct already-resolved participants if needed
+     */
+    public void testFullRefreshResolvedSingleMatch() {
+        addParticipant("650-123-1233", 1, "Joh", "content://photo/joh");
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_FULL);
+        verifyParticipant("650-123-1233", 1, "John", "content://photo/john");
+    }
+
+    /**
+     * Verify that incremental refresh will not touch participant that is marked as not found.
+     */
+    public void testIncrementalRefreshNotFound() {
+        addParticipant("650-123-1233", ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND,
+                null, null);
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_INCREMENTAL);
+        verifyParticipant("650-123-1233", ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND,
+                null, null);
+    }
+
+    /**
+     * Verify that full refresh will resolve participant that is marked as not found.
+     */
+    public void testFullRefreshNotFound() {
+        addParticipant("650-123-1233", ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND,
+                null, null);
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_FULL);
+        verifyParticipant("650-123-1233", 1, "John", "content://photo/john");
+    }
+
+    /**
+     * Verify that refresh take consideration of current contact_id when having multiple matches.
+     */
+    public void testFullRefreshResolvedMultiMatch1() {
+        addParticipant("650-123-1233", 1, "Joh", "content://photo/joh");
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null },
+                { 2L, "Joe", "content://photo/joe", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_FULL);
+        verifyParticipant("650-123-1233", 1, "John", "content://photo/john");
+    }
+
+    /**
+     * Verify that refresh take consideration of current contact_id when having multiple matches.
+     */
+    public void testFullRefreshResolvedMultiMatch2() {
+        addParticipant("650-123-1233", 2, "Joh", "content://photo/joh");
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null },
+                { 2L, "Joe", "content://photo/joe", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_FULL);
+        verifyParticipant("650-123-1233", 2, "Joe", "content://photo/joe");
+    }
+
+    /**
+     * Verify that refresh take first contact in case current contact_id no longer matches.
+     */
+    public void testFullRefreshResolvedMultiMatch3() {
+        addParticipant("650-123-1233", 3, "Joh", "content://photo/joh");
+        addPhoneLookup("650-123-1233", new Object[][] {
+                { 1L, "John", "content://photo/john", "650-123-1233", null, null, null },
+                { 2L, "Joe", "content://photo/joe", "650-123-1233", null, null, null }
+        });
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_FULL);
+        verifyParticipant("650-123-1233", 1, "John", "content://photo/john");
+    }
+
+    /**
+     * Verify that refresh take first contact in case current contact_id no longer matches.
+     */
+    public void testFullRefreshResolvedBeforeButNotFoundNow() {
+        addParticipant("650-123-1233", 3, "Joh", "content://photo/joh");
+        addPhoneLookup("650-123-1233", new Object[][] {});
+
+        ParticipantRefresh.refreshParticipants(ParticipantRefresh.REFRESH_MODE_FULL);
+        verifyParticipant("650-123-1233", ParticipantData.PARTICIPANT_CONTACT_ID_NOT_FOUND,
+                null, null);
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/action/ActionServiceSystemTest.java b/tests/src/com/android/messaging/datamodel/action/ActionServiceSystemTest.java
new file mode 100644
index 0000000..039bec9
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/action/ActionServiceSystemTest.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.action;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.Factory;
+import com.android.messaging.FakeContext;
+import com.android.messaging.FakeContext.FakeContextHost;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.BugleServiceTestCase;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
+import com.android.messaging.datamodel.action.ActionMonitor.ActionExecutedListener;
+import com.android.messaging.datamodel.action.ActionTestHelpers.ResultTracker;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubBackgroundWorker;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubConnectivityUtil;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubLoader;
+
+import java.util.ArrayList;
+
+@MediumTest
+public class ActionServiceSystemTest extends BugleServiceTestCase<ActionServiceImpl>
+        implements ActionCompletedListener, ActionExecutedListener, FakeContextHost {
+    private static final String TAG = "ActionServiceSystemTest";
+
+    static {
+        // Set flag during loading of test cases to prevent application initialization starting
+        BugleTestCase.setTestsRunning();
+    }
+
+    @Override
+    public void onActionSucceeded(final ActionMonitor monitor,
+            final Action action, final Object data, final Object result) {
+        final TestChatAction test = (TestChatAction) action;
+        assertEquals("Expect correct action parameter", parameter, test.parameter);
+        final ResultTracker tracker = (ResultTracker) data;
+        tracker.completionResult = result;
+        synchronized(tracker) {
+            tracker.notifyAll();
+        }
+    }
+
+    @Override
+    public void onActionFailed(final ActionMonitor monitor, final Action action,
+            final Object data, final Object result) {
+        final TestChatAction test = (TestChatAction) action;
+        assertEquals("Expect correct action parameter", parameter, test.parameter);
+        final ResultTracker tracker = (ResultTracker) data;
+        tracker.completionResult = result;
+        synchronized(tracker) {
+            tracker.notifyAll();
+        }
+    }
+
+    @Override
+    public void onActionExecuted(final ActionMonitor monitor, final Action action,
+            final Object data, final Object result) {
+        final TestChatAction test = (TestChatAction) action;
+        assertEquals("Expect correct action parameter", parameter, test.parameter);
+        final ResultTracker tracker = (ResultTracker) data;
+        tracker.executionResult = result;
+    }
+
+    public ActionServiceSystemTest() {
+        super(ActionServiceImpl.class);
+    }
+
+    public void testChatActionSucceeds() {
+        final ResultTracker tracker = new ResultTracker();
+
+        final ActionService service = DataModel.get().getActionService();
+        final TestChatActionMonitor monitor = new TestChatActionMonitor(null, tracker, this, this);
+        final TestChatAction initial = new TestChatAction(monitor.getActionKey(), parameter);
+
+        assertNull("Expect completion result to start null", tracker.completionResult);
+        assertNull("Expect execution result to start null", tracker.executionResult);
+
+        final Parcel parcel = Parcel.obtain();
+        parcel.writeParcelable(initial, 0);
+        parcel.setDataPosition(0);
+        final TestChatAction action = parcel.readParcelable(mContext.getClassLoader());
+
+        synchronized(mWorker) {
+            try {
+                action.start(monitor);
+                // Wait for callback across threads
+                mWorker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for execution", false);
+            }
+        }
+
+        assertEquals("Expect to see 1 server request queued", 1,
+                mWorker.getRequestsMade().size());
+        final Action request = mWorker.getRequestsMade().get(0);
+        assertTrue("Expect Test type", request instanceof TestChatAction);
+
+        final Bundle response = new Bundle();
+        response.putString(TestChatAction.RESPONSE_TEST, processResponseResult);
+        synchronized(tracker) {
+            try {
+                request.markBackgroundWorkStarting();
+                request.markBackgroundWorkQueued();
+
+                request.markBackgroundWorkStarting();
+                request.markBackgroundCompletionQueued();
+                service.handleResponseFromBackgroundWorker(request, response);
+                // Wait for callback across threads
+                tracker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for response processing", false);
+            }
+        }
+
+        // TODO
+        //assertEquals("Expect execution result set", executeActionResult, tracker.executionResult);
+        assertEquals("Expect completion result set", processResponseResult,
+                tracker.completionResult);
+    }
+
+    public void testChatActionFails() {
+        final ResultTracker tracker = new ResultTracker();
+
+        final ActionService service = DataModel.get().getActionService();
+        final TestChatActionMonitor monitor = new TestChatActionMonitor(null, tracker, this, this);
+        final TestChatAction action = new TestChatAction(monitor.getActionKey(), parameter);
+
+        assertNull("Expect completion result to start null", tracker.completionResult);
+        assertNull("Expect execution result to start null", tracker.executionResult);
+
+        synchronized(mWorker) {
+            try {
+                action.start(monitor);
+                // Wait for callback across threads
+                mWorker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for requests", false);
+            }
+        }
+
+        final ArrayList<Intent> intents = mContext.extractIntents();
+        assertNotNull(intents);
+        assertEquals("Expect to see one intent", intents.size(), 1);
+
+        assertEquals("Expect to see 1 server request queued", 1,
+                mWorker.getRequestsMade().size());
+        final Action request = mWorker.getRequestsMade().get(0);
+        assertTrue("Expect Test type", request instanceof TestChatAction);
+
+        synchronized(tracker) {
+            try {
+                request.markBackgroundWorkStarting();
+                request.markBackgroundWorkQueued();
+
+                request.markBackgroundWorkStarting();
+                request.markBackgroundCompletionQueued();
+                service.handleFailureFromBackgroundWorker(request, new Exception("It went wrong"));
+                // Wait for callback across threads
+                tracker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for response processing", false);
+            }
+        }
+
+        // TODO
+        //assertEquals("Expect execution result set", executeActionResult, tracker.executionResult);
+        assertEquals("Expect completion result set", processFailureResult,
+                tracker.completionResult);
+    }
+
+    public void testChatActionNoMonitor() {
+        final ActionService service = DataModel.get().getActionService();
+        final TestChatAction action =
+                new TestChatAction(Action.generateUniqueActionKey(null), parameter);
+
+        synchronized(mWorker) {
+            try {
+                action.start();
+                // Wait for callback across threads
+                mWorker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for execution", false);
+            }
+        }
+
+        assertEquals("Expect to see 1 server request queued", 1,
+                mWorker.getRequestsMade().size());
+        Action request = mWorker.getRequestsMade().get(0);
+        assertTrue("Expect Test type", request instanceof TestChatAction);
+
+        final Bundle response = new Bundle();
+        response.putString(TestChatAction.RESPONSE_TEST, processResponseResult);
+        synchronized(mWorker) {
+            try {
+                service.handleResponseFromBackgroundWorker(request, response);
+                // Wait for callback across threads
+                mWorker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for response processing", false);
+            }
+        }
+
+        assertEquals("Expect to see second server request queued",
+                2, mWorker.getRequestsMade().size());
+        request = mWorker.getRequestsMade().get(1);
+        assertTrue("Expect other type",
+                request instanceof TestChatActionOther);
+    }
+
+    public void testChatActionUnregisterListener() {
+        final ResultTracker tracker = new ResultTracker();
+
+        final ActionService service = DataModel.get().getActionService();
+        final TestChatActionMonitor monitor = new TestChatActionMonitor(null, tracker, this, this);
+        final TestChatAction action = new TestChatAction(monitor.getActionKey(), parameter);
+
+        assertNull("Expect completion result to start null", tracker.completionResult);
+        assertNull("Expect execution result to start null", tracker.executionResult);
+
+        synchronized(mWorker) {
+            try {
+                action.start(monitor);
+                // Wait for callback across threads
+                mWorker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for execution", false);
+            }
+        }
+
+        assertEquals("Expect to see 1 server request queued", 1,
+                mWorker.getRequestsMade().size());
+        final Action request = mWorker.getRequestsMade().get(0);
+        assertTrue("Expect Test type", request instanceof TestChatAction);
+
+        monitor.unregister();
+
+        final Bundle response = new Bundle();
+        synchronized(mWorker) {
+            try {
+                request.markBackgroundWorkStarting();
+                request.markBackgroundWorkQueued();
+
+                request.markBackgroundWorkStarting();
+                request.markBackgroundCompletionQueued();
+                service.handleResponseFromBackgroundWorker(request, response);
+                // Wait for callback across threads
+                mWorker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for response processing", false);
+            }
+        }
+
+        //assertEquals("Expect execution result set", executeActionResult, tracker.executionResult);
+        assertEquals("Expect completion never called", null, tracker.completionResult);
+    }
+
+    StubBackgroundWorker mWorker;
+    FakeContext mContext;
+    StubLoader mLoader;
+
+    private static final String parameter = "parameter";
+    private static final Object executeActionResult = "executeActionResult";
+    private static final String processResponseResult = "processResponseResult";
+    private static final Object processFailureResult = "processFailureResult";
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        Log.d(TAG, "ChatActionTest setUp");
+
+        mContext = new FakeContext(getContext(), this);
+        mWorker = new StubBackgroundWorker();
+        FakeFactory.registerWithFakeContext(getContext(), mContext)
+                .withDataModel(new FakeDataModel(mContext)
+                .withBackgroundWorkerForActionService(mWorker)
+                .withActionService(new ActionService())
+                .withConnectivityUtil(new StubConnectivityUtil(mContext)));
+
+        mLoader = new StubLoader();
+        setContext(Factory.get().getApplicationContext());
+    }
+
+    @Override
+    public String getServiceClassName() {
+        return ActionServiceImpl.class.getName();
+    }
+
+    @Override
+    public void startServiceForStub(final Intent intent) {
+        this.startService(intent);
+    }
+
+    @Override
+    public void onStartCommandForStub(final Intent intent, final int flags, final int startId) {
+        this.getService().onStartCommand(intent, flags, startId);
+    }
+
+    public static class TestChatAction extends Action implements Parcelable {
+        public static String RESPONSE_TEST = "response_test";
+        public static String KEY_PARAMETER = "parameter";
+
+        protected TestChatAction(final String key, final String parameter) {
+            super(key);
+            this.actionParameters.putString(KEY_PARAMETER, parameter);
+            // Cache parameter as a member variable
+            this.parameter = parameter;
+        }
+
+        // An example parameter
+        public final String parameter;
+
+        /**
+         * Process the action locally - runs on datamodel service thread
+         */
+        @Override
+        protected Object executeAction() {
+            requestBackgroundWork();
+            return executeActionResult;
+        }
+
+        /**
+         * Process the response from the server - runs on datamodel service thread
+         */
+        @Override
+        protected Object processBackgroundResponse(final Bundle response) {
+            requestBackgroundWork(new TestChatActionOther(null, parameter));
+            return response.get(RESPONSE_TEST);
+        }
+
+        /**
+         * Called in case of failures when sending requests - runs on datamodel service thread
+         */
+        @Override
+        protected Object processBackgroundFailure() {
+            return processFailureResult;
+        }
+
+        private TestChatAction(final Parcel in) {
+            super(in);
+            // Cache parameter as a member variable
+            parameter = actionParameters.getString(KEY_PARAMETER);
+        }
+
+        public static final Parcelable.Creator<TestChatAction> CREATOR
+                = new Parcelable.Creator<TestChatAction>() {
+            @Override
+            public TestChatAction createFromParcel(final Parcel in) {
+                return new TestChatAction(in);
+            }
+
+            @Override
+            public TestChatAction[] newArray(final int size) {
+                return new TestChatAction[size];
+            }
+        };
+
+        @Override
+        public void writeToParcel(final Parcel parcel, final int flags) {
+            writeActionToParcel(parcel, flags);
+        }
+    }
+
+    public static class TestChatActionOther extends Action implements Parcelable {
+        protected TestChatActionOther(final String key, final String parameter) {
+            super(generateUniqueActionKey(key));
+            this.parameter = parameter;
+        }
+
+        public final String parameter;
+
+        private TestChatActionOther(final Parcel in) {
+            super(in);
+            parameter = in.readString();
+        }
+
+        public static final Parcelable.Creator<TestChatActionOther> CREATOR
+                = new Parcelable.Creator<TestChatActionOther>() {
+            @Override
+            public TestChatActionOther createFromParcel(final Parcel in) {
+                return new TestChatActionOther(in);
+            }
+
+            @Override
+            public TestChatActionOther[] newArray(final int size) {
+                return new TestChatActionOther[size];
+            }
+        };
+
+        @Override
+        public void writeToParcel(final Parcel parcel, final int flags) {
+            writeActionToParcel(parcel, flags);
+            parcel.writeString(parameter);
+        }
+    }
+
+    /**
+     * An operation that notifies a listener upon completion
+     */
+    public static class TestChatActionMonitor extends ActionMonitor {
+        /**
+         * Create action state wrapping an BlockUserAction instance
+         * @param account - account in which to block the user
+         * @param baseKey - suggested action key from BlockUserAction
+         * @param data - optional action specific data that is handed back to listener
+         * @param listener - action completed listener
+         */
+        public TestChatActionMonitor(final String baseKey, final Object data,
+                final ActionCompletedListener completed, final ActionExecutedListener executed) {
+            super(STATE_CREATED, Action.generateUniqueActionKey(baseKey), data);
+            setCompletedListener(completed);
+            setExecutedListener(executed);
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/action/ActionServiceTest.java b/tests/src/com/android/messaging/datamodel/action/ActionServiceTest.java
new file mode 100644
index 0000000..02cddae
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/action/ActionServiceTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.action;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import com.android.messaging.Factory;
+import com.android.messaging.FakeContext;
+import com.android.messaging.FakeContext.FakeContextHost;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.BugleServiceTestCase;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.action.ActionMonitor.ActionCompletedListener;
+import com.android.messaging.datamodel.action.ActionMonitor.ActionStateChangedListener;
+import com.android.messaging.datamodel.action.ActionTestHelpers.ResultTracker;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubBackgroundWorker;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubConnectivityUtil;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubLoader;
+import com.android.messaging.util.WakeLockHelper;
+
+import java.util.ArrayList;
+
+@MediumTest
+public class ActionServiceTest extends BugleServiceTestCase<ActionServiceImpl>
+        implements FakeContextHost, ActionStateChangedListener, ActionCompletedListener {
+    private static final String TAG = "ActionServiceTest";
+
+    @Override
+    public void onActionStateChanged(final Action action, final int state) {
+        mStates.add(state);
+    }
+
+    @Override
+    public void onActionSucceeded(final ActionMonitor monitor,
+            final Action action, final Object data, final Object result) {
+        final TestChatAction test = (TestChatAction) action;
+        assertNotSame(test.dontRelyOnMe, dontRelyOnMe);
+        // This will be true - but only briefly
+        assertEquals(test.dontRelyOnMe, becauseIChange);
+
+        final ResultTracker tracker = (ResultTracker) data;
+        tracker.completionResult = result;
+        synchronized(tracker) {
+            tracker.notifyAll();
+        }
+    }
+
+    @Override
+    public void onActionFailed(final ActionMonitor monitor, final Action action,
+            final Object data, final Object result) {
+        final TestChatAction test = (TestChatAction) action;
+        assertNotSame(test.dontRelyOnMe, dontRelyOnMe);
+        // This will be true - but only briefly
+        assertEquals(test.dontRelyOnMe, becauseIChange);
+
+        final ResultTracker tracker = (ResultTracker) data;
+        tracker.completionResult = result;
+        synchronized(tracker) {
+            tracker.notifyAll();
+        }
+    }
+
+    /**
+     * For a dummy action verify that the service intent is constructed and queued correctly and
+     * that when that intent is processed it actually executes the action.
+     */
+    public void testChatServiceCreatesIntentAndExecutesAction() {
+        final ResultTracker tracker = new ResultTracker();
+
+        final TestChatActionMonitor monitor = new TestChatActionMonitor(null, tracker, this, this);
+        final TestChatAction action = new TestChatAction(monitor.getActionKey(), parameter);
+
+        action.dontRelyOnMe = dontRelyOnMe;
+        assertFalse("Expect service initially stopped", mServiceStarted);
+
+        action.start(monitor);
+
+        assertTrue("Expect service started", mServiceStarted);
+
+        final ArrayList<Intent> intents = mContext.extractIntents();
+        assertNotNull(intents);
+        assertEquals("Expect to see 1 server request queued", 1, intents.size());
+        final Intent intent = intents.get(0);
+        assertEquals("Check pid", intent.getIntExtra(WakeLockHelper.EXTRA_CALLING_PID, 0),
+                Process.myPid());
+        assertEquals("Check opcode", intent.getIntExtra(ActionServiceImpl.EXTRA_OP_CODE, 0),
+                ActionServiceImpl.OP_START_ACTION);
+        assertTrue("Check wakelock held", ActionServiceImpl.sWakeLock.isHeld(intent));
+
+        synchronized(tracker) {
+            try {
+                this.startService(intent);
+                // Wait for callback across threads
+                tracker.wait(2000);
+            } catch (final InterruptedException e) {
+                assertTrue("Interrupted waiting for response processing", false);
+            }
+        }
+
+        assertEquals("Expect three states ", mStates.size(), 3);
+        assertEquals("State-0 should be STATE_QUEUED", (int)mStates.get(0),
+                ActionMonitor.STATE_QUEUED);
+        assertEquals("State-1 should be STATE_EXECUTING", (int)mStates.get(1),
+                ActionMonitor.STATE_EXECUTING);
+        assertEquals("State-2 should be STATE_COMPLETE", (int)mStates.get(2),
+                ActionMonitor.STATE_COMPLETE);
+        // TODO: Should find a way to reliably wait, this is a bit of a hack
+        if (ActionServiceImpl.sWakeLock.isHeld(intent)) {
+            Log.d(TAG, "ActionServiceTest: waiting for wakelock release");
+            try {
+                Thread.sleep(100);
+            } catch (final InterruptedException e) {
+            }
+        }
+        assertFalse("Check wakelock released", ActionServiceImpl.sWakeLock.isHeld(intent));
+    }
+
+    StubBackgroundWorker mWorker;
+    FakeContext mContext;
+    StubLoader mLoader;
+    ActionService mService;
+
+    ArrayList<Integer> mStates;
+
+    private static final String parameter = "parameter";
+    private static final Object dontRelyOnMe = "dontRelyOnMe";
+    private static final Object becauseIChange = "becauseIChange";
+    private static final Object executeActionResult = "executeActionResult";
+    private static final Object processResponseResult = "processResponseResult";
+    private static final Object processFailureResult = "processFailureResult";
+
+    public ActionServiceTest() {
+        super(ActionServiceImpl.class);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        Log.d(TAG, "ChatActionTest setUp");
+
+        sLooper = Looper.myLooper();
+
+        mWorker = new StubBackgroundWorker();
+        mContext = new FakeContext(getContext(), this);
+        FakeFactory.registerWithFakeContext(getContext(),mContext)
+                .withDataModel(new FakeDataModel(mContext)
+                .withBackgroundWorkerForActionService(mWorker)
+                .withActionService(new ActionService())
+                .withConnectivityUtil(new StubConnectivityUtil(mContext)));
+
+        mStates = new ArrayList<Integer>();
+        setContext(Factory.get().getApplicationContext());
+    }
+
+    @Override
+    public String getServiceClassName() {
+        return ActionServiceImpl.class.getName();
+    }
+
+    boolean mServiceStarted = false;
+
+    @Override
+    public void startServiceForStub(final Intent intent) {
+        // Do nothing until later
+        assertFalse(mServiceStarted);
+        mServiceStarted = true;
+    }
+
+    @Override
+    public void onStartCommandForStub(final Intent intent, final int flags, final int startId) {
+        assertTrue(mServiceStarted);
+    }
+
+    private static Looper sLooper;
+    public static void assertRunsOnOtherThread() {
+        assertTrue (Looper.myLooper() != Looper.getMainLooper());
+        assertTrue (Looper.myLooper() != sLooper);
+    }
+
+    public static class TestChatAction extends Action implements Parcelable {
+        public static String RESPONSE_TEST = "response_test";
+        public static String KEY_PARAMETER = "parameter";
+
+        protected TestChatAction(final String key, final String parameter) {
+            super(key);
+            this.actionParameters.putString(KEY_PARAMETER, parameter);
+        }
+
+        transient Object dontRelyOnMe;
+
+        /**
+         * Process the action locally - runs on service thread
+         */
+        @Override
+        protected Object executeAction() {
+            this.dontRelyOnMe = becauseIChange;
+            assertRunsOnOtherThread();
+            return executeActionResult;
+        }
+
+        /**
+         * Process the response from the server - runs on service thread
+         */
+        @Override
+        protected Object processBackgroundResponse(final Bundle response) {
+            assertRunsOnOtherThread();
+            return processResponseResult;
+        }
+
+        /**
+         * Called in case of failures when sending requests - runs on service thread
+         */
+        @Override
+        protected Object processBackgroundFailure() {
+            assertRunsOnOtherThread();
+            return processFailureResult;
+        }
+
+        private TestChatAction(final Parcel in) {
+            super(in);
+        }
+
+        public static final Parcelable.Creator<TestChatAction> CREATOR
+                = new Parcelable.Creator<TestChatAction>() {
+            @Override
+            public TestChatAction createFromParcel(final Parcel in) {
+                return new TestChatAction(in);
+            }
+
+            @Override
+            public TestChatAction[] newArray(final int size) {
+                return new TestChatAction[size];
+            }
+        };
+
+        @Override
+        public void writeToParcel(final Parcel parcel, final int flags) {
+            writeActionToParcel(parcel, flags);
+        }
+    }
+
+    /**
+     * An operation that notifies a listener upon state changes, execution and completion
+     */
+    public static class TestChatActionMonitor extends ActionMonitor {
+        public TestChatActionMonitor(final String baseKey, final Object data,
+                final ActionStateChangedListener listener, final ActionCompletedListener executed) {
+            super(STATE_CREATED, Action.generateUniqueActionKey(baseKey), data);
+            setStateChangedListener(listener);
+            setCompletedListener(executed);
+            assertEquals("Initial state should be STATE_CREATED", mState, STATE_CREATED);
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/action/ActionTest.java b/tests/src/com/android/messaging/datamodel/action/ActionTest.java
new file mode 100644
index 0000000..aefa25e
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/action/ActionTest.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.action;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.DataModelImpl;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubChatActionMonitor;
+
+import java.util.ArrayList;
+
+@MediumTest
+public class ActionTest extends BugleTestCase {
+    private static final String parameter = "parameter";
+    private static final Object executeActionResult = "executeActionResult";
+    private static final Object processResponseResult = "processResponseResult";
+    private static final Object processFailureResult = "processFailureResult";
+
+    private static final String mActionKey = "TheActionKey";
+    private static final Object mData = "PrivateData";
+    private StubChatActionMonitor mMonitor;
+    private TestChatAction mAction;
+
+    private ArrayList<StubChatActionMonitor.StateTransition> mTransitions;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getTestContext())
+                .withDataModel(new DataModelImpl(getContext()));
+
+        mMonitor = new StubChatActionMonitor(ActionMonitor.STATE_CREATED, mActionKey,
+                mData);
+        mAction = new TestChatAction(mActionKey, parameter);
+        mTransitions = mMonitor.getTransitions();
+    }
+
+    private void verifyState(final int count, final int from, final int to) {
+        assertEquals(to, mMonitor.getState());
+        assertEquals(mTransitions.size(), count);
+        verifyTransition(count-1, from , to);
+    }
+
+    private void verifyTransition(final int index, final int from, final int to) {
+        assertTrue(mTransitions.size() > index);
+        assertEquals(mAction, mTransitions.get(index).action);
+        assertEquals(from, mTransitions.get(index).from);
+        assertEquals(to, mTransitions.get(index).to);
+    }
+
+    @SmallTest
+    public void testActionStartTransitionsCorrectly() {
+        mMonitor.setState(ActionMonitor.STATE_CREATED);
+
+        ActionMonitor.registerActionMonitor(mAction.actionKey, mMonitor);
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        mAction.markStart();
+        assertEquals("After start state: STATE_QUEUED", ActionMonitor.STATE_QUEUED,
+                mMonitor.getState());
+        verifyState(1, ActionMonitor.STATE_CREATED, ActionMonitor.STATE_QUEUED);
+
+        ActionMonitor.unregisterActionMonitor(mAction.actionKey, mMonitor);
+
+        assertFalse(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertFalse(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+    }
+
+    @SmallTest
+    public void testActionStartAssertsFromIncorrectState() {
+        mMonitor.setState(ActionMonitor.STATE_UNDEFINED);
+
+        ActionMonitor.registerActionMonitor(mAction.actionKey, mMonitor);
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        try {
+            mAction.markStart();
+            fail("Expect assertion when starting from STATE_UNDEFINED");
+        } catch (final IllegalStateException ex){
+        }
+        ActionMonitor.unregisterActionMonitor(mAction.actionKey, mMonitor);
+
+        assertFalse(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertFalse(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+    }
+
+    public void testActionTransitionsEndToEndWithRequests() {
+        assertEquals("Start state: STATE_CREATED", ActionMonitor.STATE_CREATED,
+                mMonitor.getState());
+
+        ActionMonitor.registerActionMonitor(mAction.actionKey, mMonitor);
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        mAction.markStart();
+
+        verifyState(1, ActionMonitor.STATE_CREATED, ActionMonitor.STATE_QUEUED);
+
+        mAction.markBeginExecute();
+
+        verifyState(2, ActionMonitor.STATE_QUEUED, ActionMonitor.STATE_EXECUTING);
+
+        final Object result = mAction.executeAction();
+        mAction.requestBackgroundWork();
+
+        assertEquals("Check executeAction result", result, executeActionResult);
+
+        mAction.markEndExecute(result);
+
+        verifyState(3, ActionMonitor.STATE_EXECUTING,
+                ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED);
+
+        mAction.markBackgroundWorkStarting();
+
+        verifyState(4, ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED,
+                ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION);
+
+        mAction.markBackgroundWorkQueued();
+
+        verifyState(5, ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
+                ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED);
+
+        mAction.markBackgroundWorkStarting();
+
+        verifyState(6, ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED,
+                ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION);
+
+        final Bundle response = null;
+
+        mAction.markBackgroundCompletionQueued();
+
+        verifyState(7, ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
+                ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED);
+
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        mAction.processBackgroundWorkResponse(response);
+
+        verifyTransition(7, ActionMonitor.STATE_BACKGROUND_COMPLETION_QUEUED,
+                ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE);
+
+        verifyState(9, ActionMonitor.STATE_PROCESSING_BACKGROUND_RESPONSE,
+                ActionMonitor.STATE_COMPLETE);
+
+        assertFalse(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertFalse(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+    }
+
+    public void testActionTransitionsEndToEndFailsRequests() {
+        assertEquals("Start state: STATE_CREATED", ActionMonitor.STATE_CREATED,
+                mMonitor.getState());
+
+        ActionMonitor.registerActionMonitor(mAction.actionKey, mMonitor);
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        mAction.markStart();
+
+        verifyState(1, ActionMonitor.STATE_CREATED, ActionMonitor.STATE_QUEUED);
+
+        mAction.markBeginExecute();
+
+        verifyState(2, ActionMonitor.STATE_QUEUED, ActionMonitor.STATE_EXECUTING);
+
+        final Object result = mAction.executeAction();
+        mAction.requestBackgroundWork();
+
+        assertEquals("Check executeAction result", result, executeActionResult);
+
+        mAction.markEndExecute(result);
+
+        verifyState(3, ActionMonitor.STATE_EXECUTING,
+                ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED);
+
+        mAction.markBackgroundWorkStarting();
+
+        verifyState(4, ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED,
+                ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION);
+
+        mAction.markBackgroundWorkQueued();
+
+        verifyState(5, ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
+                ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED);
+
+        mAction.markBackgroundWorkStarting();
+
+        verifyState(6, ActionMonitor.STATE_BACKGROUND_ACTIONS_QUEUED,
+                ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION);
+
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        mAction.processBackgroundWorkFailure();
+
+        verifyState(7, ActionMonitor.STATE_EXECUTING_BACKGROUND_ACTION,
+                ActionMonitor.STATE_COMPLETE);
+
+        assertFalse(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertFalse(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+    }
+
+    public void testActionTransitionsEndToEndNoRequests() {
+        assertEquals("Start state: STATE_CREATED", ActionMonitor.STATE_CREATED,
+                mMonitor.getState());
+
+        ActionMonitor.registerActionMonitor(mAction.actionKey, mMonitor);
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        mAction.markStart();
+
+        verifyState(1, ActionMonitor.STATE_CREATED, ActionMonitor.STATE_QUEUED);
+
+        mAction.markBeginExecute();
+
+        verifyState(2, ActionMonitor.STATE_QUEUED, ActionMonitor.STATE_EXECUTING);
+
+        final Object result = mAction.executeAction();
+
+        assertEquals("Check executeAction result", result, executeActionResult);
+
+        assertTrue(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertTrue(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+        assertEquals(ActionMonitor.sActionMonitors.get(mAction.actionKey), mMonitor);
+
+        mAction.markEndExecute(result);
+
+        verifyState(3, ActionMonitor.STATE_EXECUTING,
+                ActionMonitor.STATE_COMPLETE);
+
+        assertFalse(ActionMonitor.sActionMonitors.containsKey(mAction.actionKey));
+        assertFalse(ActionMonitor.sActionMonitors.containsValue(mMonitor));
+    }
+
+    public static class TestChatAction extends Action implements Parcelable {
+        protected TestChatAction(final String key, final String parameter) {
+            super(key);
+            this.parameter = parameter;
+        }
+
+        public final String parameter;
+
+        /**
+         * Process the action locally - runs on service thread
+         */
+        @Override
+        protected Object executeAction() {
+            assertEquals("Check parameter", parameter, ActionTest.parameter);
+            return executeActionResult;
+        }
+
+        /**
+         * Process the response from the server - runs on service thread
+         */
+        @Override
+        protected Object processBackgroundResponse(final Bundle response) {
+            assertEquals("Check parameter", parameter, ActionTest.parameter);
+            return processResponseResult;
+        }
+
+        /**
+         * Called in case of failures when sending requests - runs on service thread
+         */
+        @Override
+        protected Object processBackgroundFailure() {
+            assertEquals("Check parameter", parameter, ActionTest.parameter);
+            return processFailureResult;
+        }
+
+        private TestChatAction(final Parcel in) {
+            super(in);
+            parameter = in.readString();
+        }
+
+        public static final Parcelable.Creator<TestChatAction> CREATOR
+                = new Parcelable.Creator<TestChatAction>() {
+            @Override
+            public TestChatAction createFromParcel(final Parcel in) {
+                return new TestChatAction(in);
+            }
+
+            @Override
+            public TestChatAction[] newArray(final int size) {
+                return new TestChatAction[size];
+            }
+        };
+
+        @Override
+        public void writeToParcel(final Parcel parcel, final int flags) {
+            writeActionToParcel(parcel, flags);
+            parcel.writeString(parameter);
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/action/ActionTestHelpers.java b/tests/src/com/android/messaging/datamodel/action/ActionTestHelpers.java
new file mode 100644
index 0000000..d72a0f9
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/action/ActionTestHelpers.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.action;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+
+import com.android.messaging.util.ConnectivityUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ActionTestHelpers {
+    private static final String TAG = "DataModelTestHelpers";
+
+    static class StubLoader extends ContentObserver {
+        ArrayList<Uri> mUriList = new ArrayList<Uri>();
+
+        StubLoader() {
+            super(null);
+        }
+
+        public void clear() {
+            mUriList.clear();
+        }
+
+        @Override
+        public void onChange(final boolean selfChange) {
+            // Handle change.
+            mUriList.add(null);
+        }
+
+        // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
+        // Only supported on platform 16 and above...
+        @Override
+        public void onChange(final boolean selfChange, final Uri uri) {
+            // Handle change.
+            mUriList.add(uri);
+        }
+    }
+
+    static class StubBackgroundWorker extends BackgroundWorker {
+        public StubBackgroundWorker() {
+            super();
+            mActions = new ArrayList<Action>();
+        }
+
+        ArrayList<Action> mActions;
+        public ArrayList<Action> getRequestsMade() {
+            return mActions;
+        }
+
+        @Override
+        public void queueBackgroundWork(final List<Action> actions) {
+            mActions.addAll(actions);
+
+            synchronized(this) {
+                this.notifyAll();
+            }
+        }
+    }
+
+    static class ResultTracker {
+        public Object executionResult;
+        public Object completionResult;
+    }
+
+    static class StubChatActionMonitor extends ActionMonitor {
+        static public class StateTransition {
+            Action action;
+            int from;
+            int to;
+            public StateTransition(final Action action, final int from, final int to) {
+                this.action = action;
+                this.from = from;
+                this.to = to;
+            }
+        }
+
+        private final ArrayList<StateTransition> mTransitions;
+        public ArrayList<StateTransition> getTransitions() {
+            return mTransitions;
+        }
+
+        protected StubChatActionMonitor(final int initialState, final String actionKey,
+                final Object data) {
+            super(initialState, actionKey, data);
+            mTransitions =  new ArrayList<StateTransition>();
+        }
+
+        @Override
+        protected void updateState(final Action action, final int expectedState,
+                final int state) {
+            mTransitions.add(new StateTransition(action, mState, state));
+            super.updateState(action, expectedState, state);
+        }
+
+        public void setState(final int state) {
+            mState = state;
+        }
+
+        public int getState() {
+            return mState;
+        }
+    }
+
+    public static class StubActionService extends ActionService {
+        public static class StubActionServiceCallLog {
+            public final Action action;
+            public final Action request;
+            public final Bundle response;
+            public final Exception exception;
+            public final Action update;
+
+            public StubActionServiceCallLog(final Action action,
+                    final Action request,
+                    final Bundle response,
+                    final Exception exception,
+                    final Action update) {
+                this.action = action;
+                this.request = request;
+                this.response = response;
+                this.exception = exception;
+                this.update = update;
+            }
+        }
+
+        private final ArrayList<StubActionServiceCallLog> mServiceCalls =
+                new ArrayList<StubActionServiceCallLog>();
+
+        public ArrayList<StubActionServiceCallLog> getCalls() {
+            return mServiceCalls;
+        }
+
+        @Override
+        public void startAction(final Action action) {
+            mServiceCalls.add(new StubActionServiceCallLog(action, null, null, null, null));
+            synchronized(this) {
+                this.notifyAll();
+            }
+        }
+
+        @Override
+        public void handleResponseFromBackgroundWorker(final Action request,
+                final Bundle response) {
+            mServiceCalls.add(new StubActionServiceCallLog(null, request, response, null, null));
+            synchronized(this) {
+                this.notifyAll();
+            }
+        }
+
+        @Override
+        protected void handleFailureFromBackgroundWorker(final Action request,
+                final Exception exception) {
+            mServiceCalls.add(new StubActionServiceCallLog(null, request, null, exception, null));
+            synchronized(this) {
+                this.notifyAll();
+            }
+        }
+    }
+
+    public static class StubConnectivityUtil extends ConnectivityUtil {
+        public StubConnectivityUtil(final Context context) {
+            super(context);
+        }
+
+        @Override
+        public void registerForSignalStrength() {
+        }
+
+        @Override
+        public void unregisterForSignalStrength() {
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/action/GetOrCreateConversationActionTest.java b/tests/src/com/android/messaging/datamodel/action/GetOrCreateConversationActionTest.java
new file mode 100644
index 0000000..b05b022
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/action/GetOrCreateConversationActionTest.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.action;
+
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.mock.MockContentProvider;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeContext;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.BugleDatabaseOperations;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseWrapper;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubActionService;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubActionService.StubActionServiceCallLog;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.sms.MmsUtils;
+
+import org.mockito.Mock;
+
+import java.util.ArrayList;
+
+@SmallTest
+public class GetOrCreateConversationActionTest extends BugleTestCase {
+
+    @Mock GetOrCreateConversationActionListener mockListener;
+
+    public void testGetOrCreateConversation() {
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+
+        final ArrayList<String> recipients = new ArrayList<String>();
+        recipients.add("5551234567");
+        recipients.add("5551234568");
+
+        // Generate a list of partially formed participants
+        final ArrayList<ParticipantData> participants = new
+                ArrayList<ParticipantData>();
+
+
+        for (final String recipient : recipients) {
+            participants.add(ParticipantData.getFromRawPhoneBySystemLocale(recipient));
+        }
+
+        // Test that we properly stubbed the SMS provider to return a thread id
+        final long threadId = MmsUtils.getOrCreateThreadId(mContext, recipients);
+        assertEquals(TestDataFactory.SMS_MMS_THREAD_ID_CURSOR_VALUE, threadId);
+
+        final String blankId = BugleDatabaseOperations.getExistingConversation(db, threadId, false);
+        assertNull("Conversation already exists", blankId);
+
+        ArrayList<StubActionServiceCallLog> calls = mService.getCalls();
+
+        GetOrCreateConversationActionMonitor monitor =
+                GetOrCreateConversationAction.getOrCreateConversation(participants, null,
+                        mockListener);
+
+        assertEquals("Failed to start service once for action", calls.size(), 1);
+        assertTrue("Action not GetOrCreateConversationAction", calls.get(0).action instanceof
+                GetOrCreateConversationAction);
+
+        GetOrCreateConversationAction action = (GetOrCreateConversationAction)
+                calls.get(0).action;
+
+        Object result = action.executeAction();
+
+        assertTrue(result instanceof String);
+
+        // Make sure that we created a new conversation
+        assertEquals(TestDataFactory.NUM_TEST_CONVERSATIONS+1, Integer.parseInt((String)result));
+
+        // Now get the conversation that we just created again
+        monitor = GetOrCreateConversationAction.getOrCreateConversation(participants, null,
+                        mockListener);
+
+        calls = mService.getCalls();
+        assertEquals("Failed to start service for second action", calls.size(), 2);
+        assertTrue("Action not GetOrCreateConversationAction", calls.get(1).action instanceof
+                GetOrCreateConversationAction);
+        action = (GetOrCreateConversationAction)calls.get(1).action;
+        result = action.executeAction();
+
+        assertTrue(result instanceof String);
+
+        final String conversationId = (String) result;
+
+        // Make sure that we found the same conversation id
+        assertEquals(TestDataFactory.NUM_TEST_CONVERSATIONS+1, Integer.parseInt((String)result));
+
+        final ArrayList<ParticipantData> conversationParticipants =
+                BugleDatabaseOperations.getParticipantsForConversation(db, conversationId);
+
+        assertEquals("Participant count mismatch", recipients.size(),
+                conversationParticipants.size());
+        for(final ParticipantData participant : conversationParticipants) {
+            assertTrue(recipients.contains(participant.getSendDestination()));
+        }
+
+        final Uri conversationParticipantsUri =
+                MessagingContentProvider.buildConversationParticipantsUri(conversationId);
+        final Cursor cursor = mContext.getContentResolver().query(conversationParticipantsUri,
+                ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
+
+        int countSelf = 0;
+        while(cursor.moveToNext()) {
+            final ParticipantData participant = ParticipantData.getFromCursor(cursor);
+            if (participant.isSelf()) {
+                countSelf++;
+            } else {
+                assertTrue(recipients.contains(participant.getSendDestination()));
+            }
+        }
+        cursor.close();
+        assertEquals("Expect one self participant in conversations", 1, countSelf);
+        assertEquals("Cursor count mismatch", recipients.size(), cursor.getCount() - countSelf);
+
+        final String realId = BugleDatabaseOperations.getExistingConversation(db, threadId, false);
+        assertEquals("Conversation already exists", realId, conversationId);
+    }
+
+    private FakeContext mContext;
+    private StubActionService mService;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        mContext = new FakeContext(getTestContext());
+
+        final MockContentProvider mockProvider = new MockContentProvider() {
+            @Override
+            public Cursor query(final Uri uri, final String[] projection, final String selection,
+                    final String[] selectionArgs, final String sortOrder) {
+                return TestDataFactory.getSmsMmsThreadIdCursor();
+            }
+        };
+
+        mContext.addContentProvider("mms-sms", mockProvider);
+        final MessagingContentProvider provider = new MessagingContentProvider();
+        final ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.authority = MessagingContentProvider.AUTHORITY;
+        provider.attachInfo(mContext, providerInfo);
+        mContext.addContentProvider(MessagingContentProvider.AUTHORITY, provider);
+
+        mService = new StubActionService();
+        final FakeDataModel fakeDataModel = new FakeDataModel(mContext)
+            .withActionService(mService);
+        FakeFactory.registerWithFakeContext(getTestContext(), mContext)
+                .withDataModel(fakeDataModel);
+        provider.setDatabaseForTest(fakeDataModel.getDatabase());
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/action/ReadWriteDraftMessageActionTest.java b/tests/src/com/android/messaging/datamodel/action/ReadWriteDraftMessageActionTest.java
new file mode 100644
index 0000000..0405c90
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/action/ReadWriteDraftMessageActionTest.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.action;
+
+import android.content.ContentProvider;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeContext;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.BugleDatabaseOperations;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseHelper;
+import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
+import com.android.messaging.datamodel.DatabaseWrapper;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.MediaScratchFileProvider;
+import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubActionService;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubActionService.StubActionServiceCallLog;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubConnectivityUtil;
+import com.android.messaging.datamodel.action.ReadDraftDataAction.ReadDraftDataActionListener;
+import com.android.messaging.datamodel.data.MessageData;
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.util.ContentType;
+
+import org.mockito.Mock;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+@SmallTest
+public class ReadWriteDraftMessageActionTest extends BugleTestCase {
+
+    @Mock ReadDraftDataActionListener mockListener;
+
+    // TODO: Add test cases
+    //  1. Make sure drafts can include attachments and multiple parts
+    //  2. Make sure attachments get cleaned up appropriately
+    //  3. Make sure messageId and partIds not reused (currently each draft is a new message).
+    public void testWriteDraft() {
+        final String draftMessage = "draftMessage";
+        final long threadId = 1234567;
+        final boolean senderBlocked = false;
+        final String participantNumber = "5551234567";
+
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+
+        final String conversationId = getOrCreateConversation(db, participantNumber, threadId,
+                senderBlocked);
+        final String selfId = getOrCreateSelfId(db);
+
+        // Should clear/stub DB
+        final ArrayList<StubActionServiceCallLog> calls = mService.getCalls();
+
+        final MessageData message = MessageData.createDraftSmsMessage(conversationId, selfId,
+                draftMessage);
+
+        WriteDraftMessageAction.writeDraftMessage(conversationId, message);
+
+        assertEquals("Failed to start service once for action", calls.size(), 1);
+        assertTrue("Action not SaveDraftMessageAction",
+                calls.get(0).action instanceof WriteDraftMessageAction);
+
+        final Action save = calls.get(0).action;
+
+        final Object result = save.executeAction();
+
+        assertTrue("Expect row number string as result", result instanceof String);
+        final String messageId = (String) result;
+
+        // Should check DB
+        final MessageData actual = BugleDatabaseOperations.readMessage(db, messageId);
+        assertNotNull("Database missing draft", actual);
+        assertEquals("Draft text changed", draftMessage, actual.getMessageText());
+    }
+
+    private static String getOrCreateSelfId(final DatabaseWrapper db) {
+        db.beginTransaction();
+        final String selfId = BugleDatabaseOperations.getOrCreateParticipantInTransaction(db,
+                ParticipantData.getSelfParticipant(ParticipantData.DEFAULT_SELF_SUB_ID));
+        db.setTransactionSuccessful();
+        db.endTransaction();
+        return selfId;
+    }
+
+    private static String getOrCreateConversation(final DatabaseWrapper db,
+            final String participantNumber, final long threadId, final boolean senderBlocked) {
+        final ArrayList<ParticipantData> participants =
+                new ArrayList<ParticipantData>();
+        participants.add(ParticipantData.getFromRawPhoneBySystemLocale(participantNumber));
+
+        final String conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
+                senderBlocked, participants, false, false, null);
+        assertNotNull("No conversation", conversationId);
+        return conversationId;
+    }
+
+    public void testReadDraft() {
+        final Object data = "data";
+        final String draftMessage = "draftMessage";
+        final long threadId = 1234567;
+        final boolean senderBlocked = false;
+        final String participantNumber = "5552345678";
+
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+
+        final String conversationId = getOrCreateConversation(db, participantNumber, threadId,
+                senderBlocked);
+        final String selfId = getOrCreateSelfId(db);
+
+        // Should clear/stub DB
+        final ArrayList<StubActionServiceCallLog> calls = mService.getCalls();
+
+        final MessageData message = MessageData.createDraftSmsMessage(conversationId, selfId,
+                draftMessage);
+
+        BugleDatabaseOperations.updateDraftMessageData(db, conversationId, message,
+                BugleDatabaseOperations.UPDATE_MODE_ADD_DRAFT);
+
+        final ActionMonitor monitor =
+                ReadDraftDataAction.readDraftData(conversationId, null, data, mockListener);
+
+        assertEquals("Unexpected number of calls to service", 1, calls.size());
+        assertTrue("Action not of type ReadDraftMessageAction",
+                calls.get(0).action instanceof ReadDraftDataAction);
+
+        final Action read = calls.get(0).action;
+
+        final Object result = read.executeAction();
+
+        assertTrue(result instanceof ReadDraftDataAction.DraftData);
+        final ReadDraftDataAction.DraftData draft = (ReadDraftDataAction.DraftData) result;
+
+        assertEquals("Draft message text differs", draftMessage, draft.message.getMessageText());
+        assertEquals("Draft self differs", selfId, draft.message.getSelfId());
+        assertEquals("Draft conversation differs", conversationId,
+                draft.conversation.getConversationId());
+    }
+
+    public void testReadDraftForNewConversation() {
+        final Object data = "data";
+        long threadId = 1234567;
+        final boolean senderBlocked = false;
+        long phoneNumber = 5557654567L;
+        final String notConversationId = "ThisIsNotValidConversationId";
+
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+        final String selfId = getOrCreateSelfId(db);
+
+        // Unless set a new conversation should have a null draft message
+        final MessageData blank = BugleDatabaseOperations.readDraftMessageData(db,
+                notConversationId, selfId);
+        assertNull(blank);
+
+        String conversationId = null;
+        do {
+            conversationId = BugleDatabaseOperations.getExistingConversation(db,
+                    threadId, senderBlocked);
+            threadId++;
+            phoneNumber++;
+        }
+        while(!TextUtils.isEmpty(conversationId));
+
+        final ArrayList<ParticipantData> participants =
+                new ArrayList<ParticipantData>();
+        participants.add(ParticipantData.getFromRawPhoneBySystemLocale(Long.toString(phoneNumber)));
+
+        conversationId = BugleDatabaseOperations.getOrCreateConversation(db, threadId,
+                senderBlocked, participants, false, false, null);
+        assertNotNull("No conversation", conversationId);
+
+        final MessageData actual = BugleDatabaseOperations.readDraftMessageData(db, conversationId,
+                selfId);
+        assertNull(actual);
+
+        // Should clear/stub DB
+        final ArrayList<StubActionServiceCallLog> calls = mService.getCalls();
+
+        final ActionMonitor monitor =
+                ReadDraftDataAction.readDraftData(conversationId, null, data, mockListener);
+
+        assertEquals("Unexpected number of calls to service", 1, calls.size());
+        assertTrue("Action not of type ReadDraftMessageAction",
+                calls.get(0).action instanceof ReadDraftDataAction);
+
+        final Action read = calls.get(0).action;
+
+        final Object result = read.executeAction();
+
+        assertTrue(result instanceof ReadDraftDataAction.DraftData);
+        final ReadDraftDataAction.DraftData draft = (ReadDraftDataAction.DraftData) result;
+
+        assertEquals("Draft message text differs", "", draft.message.getMessageText());
+        assertEquals("Draft self differs", selfId, draft.message.getSelfId());
+        assertEquals("Draft conversation differs", conversationId,
+                draft.conversation.getConversationId());
+    }
+
+    public void testWriteAndReadDraft() {
+        final Object data = "data";
+        final String draftMessage = "draftMessage";
+
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+        final Cursor conversations = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
+                new String[] { ConversationColumns._ID, ConversationColumns.CURRENT_SELF_ID }, null,
+                null, null /* groupBy */, null /* having */, null /* orderBy */);
+
+        if (conversations.moveToFirst()) {
+            final String conversationId = conversations.getString(0);
+            final String selfId = getOrCreateSelfId(db);
+
+            // Should clear/stub DB
+            final ArrayList<StubActionServiceCallLog> calls = mService.getCalls();
+
+            final MessageData message = MessageData.createDraftSmsMessage(conversationId, selfId,
+                    draftMessage);
+
+            WriteDraftMessageAction.writeDraftMessage(conversationId, message);
+
+            assertEquals("Failed to start service once for action", calls.size(), 1);
+            assertTrue("Action not SaveDraftMessageAction",
+                    calls.get(0).action instanceof WriteDraftMessageAction);
+
+            final Action save = calls.get(0).action;
+
+            Object result = save.executeAction();
+
+            assertTrue("Expect row number string as result", result instanceof String);
+
+            // Should check DB
+
+            final ActionMonitor monitor =
+                    ReadDraftDataAction.readDraftData(conversationId, null, data,
+                            mockListener);
+
+            assertEquals("Expect two calls queued", 2, calls.size());
+            assertTrue("Expect action", calls.get(1).action instanceof ReadDraftDataAction);
+
+            final Action read = calls.get(1).action;
+
+            result = read.executeAction();
+
+            assertTrue(result instanceof ReadDraftDataAction.DraftData);
+            final ReadDraftDataAction.DraftData draft = (ReadDraftDataAction.DraftData) result;
+
+            assertEquals("Draft message text differs", draftMessage, draft.message.getMessageText());
+            // The conversation's self id is used as the draft's self id.
+            assertEquals("Draft self differs", conversations.getString(1),
+                    draft.message.getSelfId());
+            assertEquals("Draft conversation differs", conversationId,
+                    draft.conversation.getConversationId());
+        } else {
+            fail("No conversations in database");
+        }
+    }
+
+    public void testUpdateDraft() {
+        final String initialMessage = "initialMessage";
+        final String draftMessage = "draftMessage";
+        final long threadId = 1234567;
+        final boolean senderBlocked = false;
+        final String participantNumber = "5553456789";
+
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+
+        final String conversationId = getOrCreateConversation(db, participantNumber, threadId,
+                senderBlocked);
+        final String selfId = getOrCreateSelfId(db);
+
+        final ArrayList<StubActionServiceCallLog> calls = mService.getCalls();
+
+        // Insert initial message
+        MessageData initial = MessageData.createDraftSmsMessage(conversationId, selfId,
+                initialMessage);
+
+        BugleDatabaseOperations.updateDraftMessageData(db, conversationId, initial,
+                BugleDatabaseOperations.UPDATE_MODE_ADD_DRAFT);
+
+        initial = BugleDatabaseOperations.readDraftMessageData(db,
+                conversationId, selfId);
+        assertEquals("Initial text mismatch", initialMessage, initial.getMessageText());
+
+        // Now update the draft
+        final MessageData message = MessageData.createDraftSmsMessage(conversationId, selfId,
+                draftMessage);
+        WriteDraftMessageAction.writeDraftMessage(conversationId, message);
+
+        assertEquals("Failed to start service once for action", calls.size(), 1);
+        assertTrue("Action not SaveDraftMessageAction",
+                calls.get(0).action instanceof WriteDraftMessageAction);
+
+        final Action save = calls.get(0).action;
+
+        final Object result = save.executeAction();
+
+        assertTrue("Expect row number string as result", result instanceof String);
+
+        // Check DB
+        final MessageData actual =  BugleDatabaseOperations.readDraftMessageData(db,
+                conversationId, selfId);
+        assertNotNull("Database missing draft", actual);
+        assertEquals("Draft text mismatch", draftMessage, actual.getMessageText());
+        assertNull("Draft messageId should be null", actual.getMessageId());
+    }
+
+    public void testBugleDatabaseDraftOperations() {
+        final String initialMessage = "initialMessage";
+        final String draftMessage = "draftMessage";
+        final long threadId = 1234599;
+        final boolean senderBlocked = false;
+        final String participantNumber = "5553456798";
+        final String subject = "subject here";
+
+        final DatabaseWrapper db = DataModel.get().getDatabase();
+
+        final String conversationId = getOrCreateConversation(db, participantNumber, threadId,
+                senderBlocked);
+        final String selfId = getOrCreateSelfId(db);
+
+        final String text = "This is some text";
+        final Uri mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri("txt");
+        OutputStream outputStream = null;
+        try {
+            outputStream = mContext.getContentResolver().openOutputStream(mOutputUri);
+            outputStream.write(text.getBytes());
+        } catch (final FileNotFoundException e) {
+            fail("Cannot open output file");
+        } catch (final IOException e) {
+            fail("Cannot write output file");
+        }
+
+        final MessageData initial =
+                MessageData.createDraftMmsMessage(conversationId, selfId, initialMessage, subject);
+        initial.addPart(MessagePartData.createMediaMessagePart(ContentType.MULTIPART_MIXED,
+                mOutputUri, 0, 0));
+
+        final String initialMessageId = BugleDatabaseOperations.updateDraftMessageData(db,
+                conversationId, initial, BugleDatabaseOperations.UPDATE_MODE_ADD_DRAFT);
+        assertNotNull(initialMessageId);
+
+        final MessageData initialDraft = BugleDatabaseOperations.readMessage(db, initialMessageId);
+        assertNotNull(initialDraft);
+        int cnt = 0;
+        for(final MessagePartData part : initialDraft.getParts()) {
+            if (part.isAttachment()) {
+                assertEquals(part.getContentUri(), mOutputUri);
+            } else {
+                assertEquals(part.getText(), initialMessage);
+            }
+            cnt++;
+        }
+        assertEquals("Wrong number of parts", 2, cnt);
+
+        InputStream inputStream = null;
+        try {
+            inputStream = mContext.getContentResolver().openInputStream(mOutputUri);
+            final byte[] buffer = new byte[256];
+            final int read = inputStream.read(buffer);
+            assertEquals(read, text.getBytes().length);
+        } catch (final FileNotFoundException e) {
+            fail("Cannot open input file");
+        } catch (final IOException e) {
+            fail("Cannot read input file");
+        }
+
+        final String moreText = "This is some more text";
+        final Uri mAnotherUri = MediaScratchFileProvider.buildMediaScratchSpaceUri("txt");
+        outputStream = null;
+        try {
+            outputStream = mContext.getContentResolver().openOutputStream(mAnotherUri);
+            outputStream.write(moreText.getBytes());
+        } catch (final FileNotFoundException e) {
+            fail("Cannot open output file");
+        } catch (final IOException e) {
+            fail("Cannot write output file");
+        }
+
+        final MessageData another =
+                MessageData.createDraftMmsMessage(conversationId, selfId, draftMessage, subject);
+        another.addPart(MessagePartData.createMediaMessagePart(ContentType.MMS_MULTIPART_MIXED,
+                mAnotherUri, 0, 0));
+
+        final String anotherMessageId = BugleDatabaseOperations.updateDraftMessageData(db,
+                conversationId, another, BugleDatabaseOperations.UPDATE_MODE_ADD_DRAFT);
+        assertNotNull(anotherMessageId);
+
+        final MessageData anotherDraft = BugleDatabaseOperations.readMessage(db, anotherMessageId);
+        assertNotNull(anotherDraft);
+        cnt = 0;
+        for(final MessagePartData part : anotherDraft.getParts()) {
+            if (part.isAttachment()) {
+                assertEquals(part.getContentUri(), mAnotherUri);
+            } else {
+                assertEquals(part.getText(), draftMessage);
+            }
+            cnt++;
+        }
+        assertEquals("Wrong number of parts", 2, cnt);
+
+        inputStream = null;
+        try {
+            inputStream = mContext.getContentResolver().openInputStream(mOutputUri);
+            assertNull("Original draft content should have been deleted", inputStream);
+        } catch (final FileNotFoundException e) {
+        }
+        inputStream = null;
+        try {
+            inputStream = mContext.getContentResolver().openInputStream(mAnotherUri);
+            final byte[] buffer = new byte[256];
+            final int read = inputStream.read(buffer);
+            assertEquals(read, moreText.getBytes().length);
+        } catch (final FileNotFoundException e) {
+            fail("Cannot open input file");
+        } catch (final IOException e) {
+            fail("Cannot read input file");
+        }
+
+        final MessageData last = null;
+        final String lastPartId = BugleDatabaseOperations.updateDraftMessageData(db,
+                conversationId, last, BugleDatabaseOperations.UPDATE_MODE_ADD_DRAFT);
+        assertNull(lastPartId);
+
+        inputStream = null;
+        try {
+            inputStream = mContext.getContentResolver().openInputStream(mAnotherUri);
+            assertNull("Original draft content should have been deleted", inputStream);
+        } catch (final FileNotFoundException e) {
+        }
+
+    }
+
+    private StubActionService mService;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        final FakeContext context = new FakeContext(getTestContext());
+
+        final ContentProvider bugleProvider = new MessagingContentProvider();
+        final ProviderInfo bugleProviderInfo = new ProviderInfo();
+        bugleProviderInfo.authority = MessagingContentProvider.AUTHORITY;
+        bugleProvider.attachInfo(mContext, bugleProviderInfo);
+        context.addContentProvider(MessagingContentProvider.AUTHORITY, bugleProvider);
+        final ContentProvider mediaProvider = new MediaScratchFileProvider();
+        final ProviderInfo mediaProviderInfo = new ProviderInfo();
+        mediaProviderInfo.authority = MediaScratchFileProvider.AUTHORITY;
+        mediaProvider.attachInfo(mContext, mediaProviderInfo);
+        context.addContentProvider(MediaScratchFileProvider.AUTHORITY, mediaProvider);
+
+        mService = new StubActionService();
+        final FakeDataModel fakeDataModel = new FakeDataModel(context)
+                .withActionService(mService)
+                .withConnectivityUtil(new StubConnectivityUtil(context));
+        FakeFactory.registerWithFakeContext(getTestContext(), context)
+                .withDataModel(fakeDataModel);
+
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/data/ConversationMessageDataTest.java b/tests/src/com/android/messaging/datamodel/data/ConversationMessageDataTest.java
new file mode 100644
index 0000000..c6b9b20
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/data/ConversationMessageDataTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.data;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.datamodel.data.ConversationMessageData;
+import com.android.messaging.datamodel.data.MessageData;
+import com.android.messaging.datamodel.data.ConversationMessageData.ConversationMessageViewColumns;
+
+@SmallTest
+public class ConversationMessageDataTest extends BugleTestCase {
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getTestContext());
+    }
+
+    public void testBindFirstMessage() {
+        final FakeCursor testCursor = TestDataFactory.getConversationMessageCursor();
+        final ConversationMessageData data = new ConversationMessageData();
+        testCursor.moveToFirst();
+        data.bind(testCursor);
+        // TODO: Add before checking in all bound fields...
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.STATUS, 0).equals(
+                MessageData.BUGLE_STATUS_INCOMING_COMPLETE), data.getIsIncoming());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
+                0), data.getSenderProfilePhotoUri());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.SENDER_FULL_NAME, 0),
+                data.getSenderFullName());
+    }
+
+    public void testBindTwice() {
+        final FakeCursor testCursor = TestDataFactory.getConversationMessageCursor();
+        final ConversationMessageData data = new ConversationMessageData();
+        testCursor.moveToPosition(1);
+        data.bind(testCursor);
+        assertEquals(TestDataFactory.getMessageText(testCursor, 1), data.getText());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.RECEIVED_TIMESTAMP, 1),
+                data.getReceivedTimeStamp());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.STATUS, 1).equals(
+                MessageData.BUGLE_STATUS_INCOMING_COMPLETE), data.getIsIncoming());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
+                1), data.getSenderProfilePhotoUri());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.SENDER_FULL_NAME, 1),
+                data.getSenderFullName());
+        testCursor.moveToPosition(2);
+        data.bind(testCursor);
+        assertEquals(TestDataFactory.getMessageText(testCursor, 2), data.getText());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.RECEIVED_TIMESTAMP, 2),
+                data.getReceivedTimeStamp());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.STATUS, 2).equals(
+                MessageData.BUGLE_STATUS_INCOMING_COMPLETE), data.getIsIncoming());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
+                2), data.getSenderProfilePhotoUri());
+        assertEquals(testCursor.getAt(ConversationMessageViewColumns.SENDER_FULL_NAME, 2),
+                data.getSenderFullName());
+    }
+
+    public void testMessageClustering() {
+        final FakeCursor testCursor = TestDataFactory.getConversationMessageCursor();
+        final ConversationMessageData data = new ConversationMessageData();
+        testCursor.moveToPosition(0);
+        data.bind(testCursor);
+        assertFalse(data.getCanClusterWithPreviousMessage());
+        assertFalse(data.getCanClusterWithNextMessage());
+
+        testCursor.moveToPosition(1);
+        data.bind(testCursor);
+        assertFalse(data.getCanClusterWithPreviousMessage());
+        assertFalse(data.getCanClusterWithNextMessage());
+
+        testCursor.moveToPosition(2);
+        data.bind(testCursor);
+        assertFalse(data.getCanClusterWithPreviousMessage());
+        assertTrue(data.getCanClusterWithNextMessage());  // 2 and 3 can be clustered
+        testCursor.moveToPosition(3);
+
+        data.bind(testCursor);
+        assertTrue(data.getCanClusterWithPreviousMessage());  // 2 and 3 can be clustered
+        assertFalse(data.getCanClusterWithNextMessage());
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/data/ConversationParticipantsDataTest.java b/tests/src/com/android/messaging/datamodel/data/ConversationParticipantsDataTest.java
new file mode 100644
index 0000000..527a600
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/data/ConversationParticipantsDataTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.data;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
+import com.android.messaging.datamodel.data.ConversationParticipantsData;
+import com.android.messaging.datamodel.data.ParticipantData;
+
+@SmallTest
+public class ConversationParticipantsDataTest extends BugleTestCase {
+    public void testBindParticipants() {
+        final FakeCursor testCursor = TestDataFactory.getConversationParticipantsCursor();
+        final ConversationParticipantsData data = new ConversationParticipantsData();
+        data.bind(testCursor);
+
+        assertEquals(data.getParticipantListExcludingSelf().size(), testCursor.getCount());
+        final ParticipantData participant2 = data.getParticipantById("2");
+        assertNotNull(participant2);
+        assertEquals(participant2.getFirstName(), testCursor.getAt(
+                ParticipantColumns.FIRST_NAME, 1) );
+        assertEquals(participant2.getSendDestination(), testCursor.getAt(
+                ParticipantColumns.SEND_DESTINATION, 1));
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/data/TestDataFactory.java b/tests/src/com/android/messaging/datamodel/data/TestDataFactory.java
new file mode 100644
index 0000000..8527e2b
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/data/TestDataFactory.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.data;
+
+import android.content.ContentValues;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.MediaStore.Images.Media;
+
+import com.android.messaging.datamodel.BugleDatabaseOperations;
+import com.android.messaging.datamodel.DatabaseHelper;
+import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
+import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
+import com.android.messaging.datamodel.DatabaseHelper.PartColumns;
+import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.datamodel.data.ConversationListItemData.ConversationListViewColumns;
+import com.android.messaging.datamodel.data.ConversationMessageData.ConversationMessageViewColumns;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.ContactUtil;
+import com.android.messaging.util.ContentType;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A factory for fake objects that can be useful for multiple tests.
+ */
+public class TestDataFactory {
+    private final static String[] sConversationListCursorColumns = new String[] {
+        ConversationListViewColumns._ID,
+        ConversationListViewColumns.NAME,
+        ConversationListViewColumns.ICON,
+        ConversationListViewColumns.SNIPPET_TEXT,
+        ConversationListViewColumns.PREVIEW_URI,
+        ConversationListViewColumns.SORT_TIMESTAMP,
+        ConversationListViewColumns.READ,
+        ConversationListViewColumns.PREVIEW_CONTENT_TYPE,
+        ConversationListViewColumns.MESSAGE_STATUS,
+    };
+
+    private final static String[] sContactCursorColumns = new String[] {
+            Phone.CONTACT_ID,
+            Phone.DISPLAY_NAME_PRIMARY,
+            Phone.PHOTO_THUMBNAIL_URI,
+            Phone.NUMBER,
+            Phone.TYPE,
+            Phone.LABEL,
+            Phone.LOOKUP_KEY,
+            Phone._ID,
+            Phone.SORT_KEY_PRIMARY,
+    };
+
+    private final static String[] sFrequentContactCursorColumns = new String[] {
+            Contacts._ID,
+            Contacts.DISPLAY_NAME,
+            Contacts.PHOTO_URI,
+            Phone.LOOKUP_KEY,
+    };
+
+    private final static String[] sConversationMessageCursorColumns = new String[] {
+        ConversationMessageViewColumns._ID,
+        ConversationMessageViewColumns.CONVERSATION_ID,
+        ConversationMessageViewColumns.PARTICIPANT_ID,
+        ConversationMessageViewColumns.SENT_TIMESTAMP,
+        ConversationMessageViewColumns.RECEIVED_TIMESTAMP,
+        ConversationMessageViewColumns.STATUS,
+        ConversationMessageViewColumns.SENDER_FULL_NAME,
+        ConversationMessageViewColumns.SENDER_PROFILE_PHOTO_URI,
+        ConversationMessageViewColumns.PARTS_IDS,
+        ConversationMessageViewColumns.PARTS_CONTENT_TYPES,
+        ConversationMessageViewColumns.PARTS_CONTENT_URIS,
+        ConversationMessageViewColumns.PARTS_WIDTHS,
+        ConversationMessageViewColumns.PARTS_HEIGHTS,
+        ConversationMessageViewColumns.PARTS_TEXTS,
+        ConversationMessageViewColumns.PARTS_COUNT
+    };
+
+    private final static String[] sGalleryCursorColumns = new String[] {
+        Media._ID,
+        Media.DATA,
+        Media.WIDTH,
+        Media.HEIGHT,
+        Media.MIME_TYPE
+    };
+
+    public static FakeCursor getConversationListCursor() {
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { Long.valueOf(1), "name1", "content://icon1",
+                        "snippetText1", "content://snippetUri1", Long.valueOf(10), 1,
+                        ContentType.IMAGE_JPEG, MessageData.BUGLE_STATUS_INCOMING_COMPLETE},
+                new Object[] { Long.valueOf(2), "name2", "content://icon2",
+                        "snippetText2", "content://snippetUri2", Long.valueOf(20) + 24*60*60*1000,
+                        0, ContentType.IMAGE_JPEG, MessageData.BUGLE_STATUS_INCOMING_COMPLETE},
+                new Object[] { Long.valueOf(3), "name3", "content://icon3",
+                        "snippetText3", "content://snippetUri3", Long.valueOf(30) + 2*24*60*60*1000,
+                        0, ContentType.IMAGE_JPEG, MessageData.BUGLE_STATUS_OUTGOING_COMPLETE}
+        };
+        return new FakeCursor(ConversationListItemData.PROJECTION, sConversationListCursorColumns,
+                cursorData);
+    }
+    public static final int CONVERSATION_LIST_CURSOR_READ_MESSAGE_INDEX = 0;
+    public static final int CONVERSATION_LIST_CURSOR_UNREAD_MESSAGE_INDEX = 1;
+
+    public static FakeCursor getEmptyConversationListCursor() {
+        return new FakeCursor(ConversationListItemData.PROJECTION, sConversationListCursorColumns,
+                new Object[][] {});
+    }
+
+    public static FakeCursor getConversationMessageCursor() {
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { Long.valueOf(0), Long.valueOf(1), Long.valueOf(1),
+                        Long.valueOf(10), Long.valueOf(10),
+                        MessageData.BUGLE_STATUS_INCOMING_COMPLETE, "Alice", null,
+                        "0", "text/plain", "''", -1, -1, "msg0", 1},
+                new Object[] { Long.valueOf(1), Long.valueOf(1), Long.valueOf(2),
+                        Long.valueOf(20), Long.valueOf(20),
+                        MessageData.BUGLE_STATUS_OUTGOING_COMPLETE, "Bob", null,
+                        "1", "text/plain", "''", -1, -1, "msg1", 1},
+                new Object[] { Long.valueOf(2), Long.valueOf(1), Long.valueOf(1),
+                        Long.valueOf(30), Long.valueOf(30),
+                        MessageData.BUGLE_STATUS_OUTGOING_COMPLETE, "Alice", null,
+                        "2", "contentType3", "'content://fakeUri3'", "0", "0", "msg1", 1},
+                new Object[] { Long.valueOf(3), Long.valueOf(1), Long.valueOf(1),
+                        Long.valueOf(40), Long.valueOf(40),
+                        MessageData.BUGLE_STATUS_OUTGOING_COMPLETE, "Alice", null,
+                        "3|4", "'contentType4'|'text/plain'", "'content://fakeUri4'|''", "0|-1", "0|-1", "''|'msg3'", 2},
+        };
+        return new FakeCursor(
+                ConversationMessageData.getProjection(),
+                sConversationMessageCursorColumns,
+                cursorData);
+    }
+
+    public static String getMessageText(final FakeCursor messageCursor, final int row) {
+        final String allPartsText = messageCursor.getAt(ConversationMessageViewColumns.PARTS_TEXTS, row)
+                .toString();
+        final int partsCount = (Integer) messageCursor.getAt(
+                ConversationMessageViewColumns.PARTS_COUNT, row);
+        final String messageId = messageCursor.getAt(
+                ConversationMessageViewColumns._ID, row).toString();
+        final List<MessagePartData> parts = ConversationMessageData.makeParts(
+                messageCursor.getAt(ConversationMessageViewColumns.PARTS_IDS, row).toString(),
+                messageCursor.getAt(ConversationMessageViewColumns.PARTS_CONTENT_TYPES, row).toString(),
+                messageCursor.getAt(ConversationMessageViewColumns.PARTS_CONTENT_URIS, row).toString(),
+                messageCursor.getAt(ConversationMessageViewColumns.PARTS_WIDTHS, row).toString(),
+                messageCursor.getAt(ConversationMessageViewColumns.PARTS_HEIGHTS, row).toString(),
+                messageCursor.getAt(ConversationMessageViewColumns.PARTS_TEXTS, row).toString(),
+                partsCount,
+                messageId);
+
+        for (final MessagePartData part : parts) {
+            if (part.isText()) {
+                return part.getText();
+            }
+        }
+        return null;
+    }
+
+    // Indexes where to find consecutive and non consecutive messages from same participant
+    // (respect to index - 1).
+    public static final int MESSAGE_WITH_SAME_PARTICIPANT_AS_PREVIOUS = 3;
+    public static final int MESSAGE_WITH_DIFFERENT_PARTICIPANT_AS_PREVIOUS = 2;
+
+    public static FakeCursor getConversationParticipantsCursor() {
+        final String[] sConversationParticipantsCursorColumns = new String[] {
+                ParticipantColumns._ID,
+                ParticipantColumns.SUB_ID,
+                ParticipantColumns.NORMALIZED_DESTINATION,
+                ParticipantColumns.SEND_DESTINATION,
+                ParticipantColumns.FULL_NAME,
+                ParticipantColumns.FIRST_NAME,
+                ParticipantColumns.PROFILE_PHOTO_URI,
+        };
+
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { 1, ParticipantData.OTHER_THAN_SELF_SUB_ID, "+15554567890",
+                        "(555)456-7890", "alice in wonderland", "alice", "alice.png" },
+                new Object[] { 2, ParticipantData.OTHER_THAN_SELF_SUB_ID, "+15551011121",
+                        "(555)101-1121", "bob the baker", "bob", "bob.png"},
+                new Object[] { 3, ParticipantData.OTHER_THAN_SELF_SUB_ID, "+15551314152",
+                        "(555)131-4152", "charles in charge", "charles", "charles.png" },
+        };
+
+        return new FakeCursor(ParticipantData.ParticipantsQuery.PROJECTION,
+                sConversationParticipantsCursorColumns, cursorData);
+    }
+
+    public static final int CONTACT_LIST_CURSOR_FIRST_LEVEL_CONTACT_INDEX = 0;
+    public static final int CONTACT_LIST_CURSOR_SECOND_LEVEL_CONTACT_INDEX = 2;
+
+    /**
+     * Returns a cursor for the all contacts list consumable by ContactPickerFragment.
+     */
+    public static FakeCursor getAllContactListCursor() {
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { Long.valueOf(0), "John Smith", "content://uri1",
+                        "425-555-1234", Phone.TYPE_HOME, "", "0", Long.valueOf(0), 0 },
+                new Object[] { Long.valueOf(1), "Sun Woo Kong", "content://uri2",
+                        "425-555-1235", Phone.TYPE_MOBILE, "", "1", Long.valueOf(1), 1 },
+                new Object[] { Long.valueOf(1), "Sun Woo Kong", "content://uri2",
+                        "425-555-1238", Phone.TYPE_HOME, "", "1", Long.valueOf(2), 2 },
+                new Object[] { Long.valueOf(2), "Anna Kinney", "content://uri3",
+                        "425-555-1236", Phone.TYPE_MAIN, "", "3", Long.valueOf(3), 3 },
+                new Object[] { Long.valueOf(3), "Mike Jones", "content://uri3",
+                        "425-555-1236", Phone.TYPE_MAIN, "", "5", Long.valueOf(4), 4 },
+        };
+        return new FakeCursor(ContactUtil.PhoneQuery.PROJECTION, sContactCursorColumns,
+                cursorData);
+    }
+
+    /**
+     * Returns a cursor for the frequent contacts list consumable by ContactPickerFragment.
+     * Note: make it so that this cursor is the generated result of getStrequentContactsCursor()
+     * and getAllContactListCursor(), i.e., expand the entries in getStrequentContactsCursor()
+     * with the details from getAllContactListCursor()
+     */
+    public static FakeCursor getFrequentContactListCursor() {
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { Long.valueOf(2), "Anna Kinney", "content://uri3",
+                        "425-555-1236", Phone.TYPE_MAIN, "", "3", Long.valueOf(3), 0 },
+                new Object[] { Long.valueOf(1), "Sun Woo Kong", "content://uri2",
+                        "425-555-1235", Phone.TYPE_MOBILE, "", "1", Long.valueOf(1), 1},
+                new Object[] { Long.valueOf(1), "Sun Woo Kong", "content://uri2",
+                        "425-555-1238", Phone.TYPE_HOME, "", "1", Long.valueOf(2), 2 },
+                new Object[] { Long.valueOf(0), "John Smith", "content://uri1",
+                        "425-555-1234", Phone.TYPE_HOME, "", "0", Long.valueOf(0), 3 },
+        };
+        return new FakeCursor(ContactUtil.PhoneQuery.PROJECTION, sContactCursorColumns,
+                cursorData);
+    }
+
+    /**
+     * Returns a strequent (starred + frequent) cursor (like the one produced by android contact
+     * provider's CONTENT_STREQUENT_URI query) that's consumable by FrequentContactsCursorBuilder.
+     */
+    public static FakeCursor getStrequentContactsCursor() {
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { Long.valueOf(0), "Anna Kinney", "content://uri1", "3" },
+                new Object[] { Long.valueOf(1), "Sun Woo Kong", "content://uri2", "1" },
+                new Object[] { Long.valueOf(2), "John Smith", "content://uri3", "0" },
+                // Email-only entry that shouldn't be included in the result.
+                new Object[] { Long.valueOf(3), "Email Contact", "content://uri4", "100" },
+        };
+        return new FakeCursor(ContactUtil.FrequentContactQuery.PROJECTION,
+                sFrequentContactCursorColumns, cursorData);
+    }
+
+    public static final int SMS_MMS_THREAD_ID_CURSOR_VALUE = 123456789;
+
+    public static FakeCursor getSmsMmsThreadIdCursor() {
+        final String[] ID_PROJECTION = { BaseColumns._ID };
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { Long.valueOf(SMS_MMS_THREAD_ID_CURSOR_VALUE) },
+        };
+        return new FakeCursor(ID_PROJECTION, ID_PROJECTION, cursorData);
+    }
+
+    public static FakeCursor getGalleryGridCursor() {
+        final Object[][] cursorData = new Object[][] {
+                new Object[] { Long.valueOf(0), "/sdcard/image1", 100, 100, "image/jpeg" },
+                new Object[] { Long.valueOf(1), "/sdcard/image2", 200, 200, "image/png" },
+                new Object[] { Long.valueOf(2), "/sdcard/image3", 300, 300, "image/jpeg" },
+        };
+        return new FakeCursor(GalleryGridItemData.IMAGE_PROJECTION, sGalleryCursorColumns,
+                cursorData);
+    }
+
+    public static final int NUM_TEST_CONVERSATIONS = 10;
+
+    /**
+     * Create test data in our db.
+     *
+     * Ideally this will create more realistic data with more variety.
+     */
+    public static void createTestData(final SQLiteDatabase db) {
+        BugleDatabaseOperations.clearParticipantIdCache();
+
+        // Timestamp for 1 day ago
+        final long yesterday = System.currentTimeMillis() - (24 * 60 * 60 * 1000);
+
+        final ContentValues conversationValues = new ContentValues();
+        for (int i = 1; i <= NUM_TEST_CONVERSATIONS; i++) {
+            conversationValues.put(ConversationColumns.NAME, "Conversation " + i);
+            final long conversationId = db.insert(DatabaseHelper.CONVERSATIONS_TABLE, null,
+                    conversationValues);
+
+            final ContentValues messageValues = new ContentValues();
+            for (int m = 1; m <= 25; m++) {
+                // Move forward ten minutes per conversation, 1 minute per message.
+                final long messageTime = yesterday + (i * 10 * 60 * 1000) + (m * 60 * 1000);
+                messageValues.put(MessageColumns.RECEIVED_TIMESTAMP, messageTime);
+                messageValues.put(MessageColumns.CONVERSATION_ID, conversationId);
+                messageValues.put(MessageColumns.SENDER_PARTICIPANT_ID,
+                        Math.abs(("" + messageTime).hashCode()) % 2);
+                final long messageId = db.insert(DatabaseHelper.MESSAGES_TABLE, null, messageValues);
+
+                // Create a text part for this message
+                final ContentValues partValues = new ContentValues();
+                partValues.put(PartColumns.MESSAGE_ID, messageId);
+                partValues.put(PartColumns.CONVERSATION_ID, conversationId);
+                partValues.put(PartColumns.TEXT, "Conversation: " + conversationId +
+                        " Message: " + m);
+                db.insert(DatabaseHelper.PARTS_TABLE, null, partValues);
+
+                // Update the snippet for this conversation to the latest message inserted
+                conversationValues.clear();
+                conversationValues.put(ConversationColumns.LATEST_MESSAGE_ID, messageId);
+                final int updatedCount = db.update(DatabaseHelper.CONVERSATIONS_TABLE,
+                        conversationValues,
+                        "_id=?", new String[]{String.valueOf(conversationId)});
+                Assert.isTrue(updatedCount == 1);
+            }
+        }
+    }
+
+    public static List<MessagePartData> getTestDraftAttachments() {
+        final MessagePartData[] retParts = new MessagePartData[] {
+                new MessagePartData(ContentType.IMAGE_JPEG, Uri.parse("content://image"),
+                        100, 100),
+                new MessagePartData(ContentType.VIDEO_3GPP, Uri.parse("content://video"),
+                        100, 100),
+                new MessagePartData(ContentType.TEXT_VCARD, Uri.parse("content://vcard"),
+                        0, 0),
+                new MessagePartData(ContentType.AUDIO_3GPP, Uri.parse("content://audio"),
+                        0, 0)
+        };
+        return Arrays.asList(retParts);
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/media/FakeImageRequest.java b/tests/src/com/android/messaging/datamodel/media/FakeImageRequest.java
new file mode 100644
index 0000000..7f6ac3f
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/media/FakeImageRequest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.text.TextUtils;
+
+import java.util.List;
+
+public class FakeImageRequest implements MediaRequest<FakeImageResource> {
+    public static final String INVALID_KEY = "invalid";
+    private final String mKey;
+    private final int mSize;
+
+    public FakeImageRequest(final String key, final int size) {
+        mKey = key;
+        mSize = size;
+    }
+
+    @Override
+    public String getKey() {
+        return mKey;
+    }
+
+    @Override
+    public FakeImageResource loadMediaBlocking(List<MediaRequest<FakeImageResource>> chainedTask)
+            throws Exception {
+        if (TextUtils.equals(mKey, INVALID_KEY)) {
+            throw new Exception();
+        } else {
+            return new FakeImageResource(mSize, mKey);
+        }
+    }
+
+    @Override
+    public int getCacheId() {
+        return FakeMediaCacheManager.FAKE_IMAGE_CACHE;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public MediaCache<FakeImageResource> getMediaCache() {
+        return (MediaCache<FakeImageResource>) MediaCacheManager.get().getOrCreateMediaCacheById(
+                getCacheId());
+    }
+
+    @Override
+    public int getRequestType() {
+        return MediaRequest.REQUEST_LOAD_MEDIA;
+    }
+
+    @Override
+    public MediaRequestDescriptor<FakeImageResource> getDescriptor() {
+        return null;
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/media/FakeImageResource.java b/tests/src/com/android/messaging/datamodel/media/FakeImageResource.java
new file mode 100644
index 0000000..969854f
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/media/FakeImageResource.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+public class FakeImageResource extends RefCountedMediaResource {
+    private boolean mClosed = false;
+    private boolean mCached = false;
+    private final int mSize;
+    private final String mImageId;
+
+    public FakeImageResource(final int size, final String imageId) {
+        super(null);
+        mSize = size;
+        mImageId = imageId;
+    }
+
+    public boolean isClosed() {
+        return mClosed;
+    }
+
+    public String getImageId() {
+        return mImageId;
+    }
+
+    public void setCached(final boolean cached) {
+        mCached = cached;
+    }
+
+    public boolean getCached() {
+        return mCached;
+    }
+
+    @Override
+    public int getMediaSize() {
+        return mSize;
+    }
+
+    @Override
+    protected void close() {
+        mClosed = true;
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/media/FakeMediaCacheManager.java b/tests/src/com/android/messaging/datamodel/media/FakeMediaCacheManager.java
new file mode 100644
index 0000000..34b3a55
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/media/FakeMediaCacheManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+public class FakeMediaCacheManager extends MediaCacheManager {
+    // List of available fake cache ids.
+    public static final int FAKE_IMAGE_CACHE = 1;
+    public static final int FAKE_BATCH_IMAGE_CACHE = 2;
+
+    @Override
+    public MediaCache<?> createMediaCacheById(final int id) {
+        switch (id) {
+            case FAKE_IMAGE_CACHE:
+                // Make a cache of only 3 KB of data.
+                return new MediaCache<FakeImageResource>(3, FAKE_IMAGE_CACHE, "FakeImageCache");
+
+            case FAKE_BATCH_IMAGE_CACHE:
+                return new MediaCache<FakeImageResource>(10, FAKE_BATCH_IMAGE_CACHE,
+                        "FakeBatchImageCache");
+        }
+        return null;
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/media/ImageRequestTest.java b/tests/src/com/android/messaging/datamodel/media/ImageRequestTest.java
new file mode 100644
index 0000000..2cfec7d
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/media/ImageRequestTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.content.ContentResolver;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.MemoryCacheManager;
+import com.android.messaging.util.ImageUtils;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Matchers;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+
+import java.io.IOException;
+
+@SmallTest
+public class ImageRequestTest extends BugleTestCase {
+    private static final int DOWNSAMPLE_IMAGE_SIZE = 2;
+
+    @Spy protected ImageUtils spyImageUtils;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getTestContext())
+                   .withMemoryCacheManager(new MemoryCacheManager())
+                   .withMediaCacheManager(new BugleMediaCacheManager());
+        spyImageUtils = Mockito.spy(new ImageUtils());
+        ImageUtils.set(spyImageUtils);
+    }
+
+    public void testLoadImageUnspecifiedSize() {
+        final String uriString = ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
+                getContext().getPackageName() + "/" + R.drawable.ic_audio_light;
+        final Uri uri = Uri.parse(uriString);
+        final UriImageRequest imageRequest = new UriImageRequest(getContext(),
+                new UriImageRequestDescriptor(uri));
+        try {
+            final ImageResource imageResource = imageRequest.loadMediaBlocking(null);
+            final ArgumentCaptor<BitmapFactory.Options> options =
+                    ArgumentCaptor.forClass(BitmapFactory.Options.class);
+            Mockito.verify(spyImageUtils).calculateInSampleSize(
+                    options.capture(),
+                    Matchers.eq(ImageRequest.UNSPECIFIED_SIZE),
+                    Matchers.eq(ImageRequest.UNSPECIFIED_SIZE));
+            assertEquals(1, options.getValue().inSampleSize);
+            assertNotNull(imageResource);
+            assertNotNull(imageResource.getBitmap());
+
+            // Make sure there's no scaling on the bitmap.
+            final int bitmapWidth = imageResource.getBitmap().getWidth();
+            final int bitmapHeight = imageResource.getBitmap().getHeight();
+            assertEquals(options.getValue().outWidth, bitmapWidth);
+            assertEquals(options.getValue().outHeight, bitmapHeight);
+        } catch (final IOException e) {
+            fail("IO exception while trying to load image resource");
+        }
+    }
+
+    public void testLoadImageWithDownsampling() {
+        final String uriString = ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
+                getContext().getPackageName() + "/" + R.drawable.ic_audio_light;
+        final Uri uri = Uri.parse(uriString);
+        final UriImageRequest imageRequest = new UriImageRequest(getContext(),
+                new UriImageRequestDescriptor(uri, DOWNSAMPLE_IMAGE_SIZE, DOWNSAMPLE_IMAGE_SIZE,
+                        false, true /* isStatic */, false /* cropToCircle */,
+                        ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
+                        ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */));
+        try {
+            final ImageResource imageResource = imageRequest.loadMediaBlocking(null);
+            final ArgumentCaptor<BitmapFactory.Options> options =
+                    ArgumentCaptor.forClass(BitmapFactory.Options.class);
+            Mockito.verify(spyImageUtils).calculateInSampleSize(
+                    options.capture(),
+                    Matchers.eq(DOWNSAMPLE_IMAGE_SIZE), Matchers.eq(DOWNSAMPLE_IMAGE_SIZE));
+            assertNotSame(1, options.getValue().inSampleSize);
+            assertNotNull(imageResource);
+            assertNotNull(imageResource.getBitmap());
+
+            // Make sure there's down sampling on the bitmap.
+            final int bitmapWidth = imageResource.getBitmap().getWidth();
+            final int bitmapHeight = imageResource.getBitmap().getHeight();
+            assertTrue(bitmapWidth >= DOWNSAMPLE_IMAGE_SIZE &&
+                    bitmapHeight >= DOWNSAMPLE_IMAGE_SIZE &&
+                    (bitmapWidth <= DOWNSAMPLE_IMAGE_SIZE * 4 ||
+                    bitmapHeight <= DOWNSAMPLE_IMAGE_SIZE * 4));
+        } catch (final IOException e) {
+            fail("IO exception while trying to load image resource");
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/datamodel/media/MediaResourceManagerTest.java b/tests/src/com/android/messaging/datamodel/media/MediaResourceManagerTest.java
new file mode 100644
index 0000000..d214067
--- /dev/null
+++ b/tests/src/com/android/messaging/datamodel/media/MediaResourceManagerTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 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.messaging.datamodel.media;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.MemoryCacheManager;
+import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener;
+
+import java.util.concurrent.CountDownLatch;
+
+@SmallTest
+public class MediaResourceManagerTest extends BugleTestCase {
+    private static final int KB = 1024;
+
+    // Loaded image resource from the MediaResourceManager callback.
+    private FakeImageResource mImageResource;
+    private BindableMediaRequest<FakeImageResource> mImageRequest;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getTestContext())
+                .withMemoryCacheManager(new MemoryCacheManager())
+                .withMediaCacheManager(new FakeMediaCacheManager());
+    }
+
+    public void testLoadFromCache() {
+        final MediaResourceManager mediaResourceManager =
+                new MediaResourceManager();
+        MediaCacheManager.get().reclaim();
+        assertNotNull(mediaResourceManager);
+
+        // Load one image of 1KB
+        loadImage(mediaResourceManager, "image1", 1 * KB, false /* shouldBeCached */, false);
+        assertEquals("image1", mImageResource.getImageId());
+        final FakeImageResource loadedResource = mImageResource;
+
+        // Load the same image.
+        loadImage(mediaResourceManager, "image1", 1 * KB, true /* shouldBeCached */, false);
+        assertEquals(loadedResource, mImageResource);
+    }
+
+    public void testCacheEviction() {
+        final MediaResourceManager mediaResourceManager =
+                new MediaResourceManager();
+        MediaCacheManager.get().reclaim();
+        assertNotNull(mediaResourceManager);
+
+        // Load one image of 1KB
+        loadImage(mediaResourceManager, "image1", 1 * KB, false /* shouldBeCached */, false);
+        assertEquals("image1", mImageResource.getImageId());
+
+        // Load another image
+        loadImage(mediaResourceManager, "image2", 2 * KB, false /* shouldBeCached */, false);
+        assertEquals("image2", mImageResource.getImageId());
+
+        // Load another image. This should fill the cache and cause eviction of image1.
+        loadImage(mediaResourceManager, "image3", 2 * KB, false /* shouldBeCached */, false);
+        assertEquals("image3", mImageResource.getImageId());
+
+        // Load image1. It shouldn't be cached any more.
+        loadImage(mediaResourceManager, "image1", 1 * KB, false /* shouldBeCached */, false);
+        assertEquals("image1", mImageResource.getImageId());
+    }
+
+    public void testReclaimMemoryFromMediaCache() {
+        final MediaResourceManager mediaResourceManager =
+                new MediaResourceManager();
+        MediaCacheManager.get().reclaim();
+        assertNotNull(mediaResourceManager);
+
+        // Load one image of 1KB
+        loadImage(mediaResourceManager, "image1", 1 * KB, false /* shouldBeCached */, false);
+        assertEquals("image1", mImageResource.getImageId());
+
+        // Purge everything from the cache, now the image should no longer be cached.
+        MediaCacheManager.get().reclaim();
+
+        // The image resource should have no ref left.
+        assertEquals(0, mImageResource.getRefCount());
+        assertTrue(mImageResource.isClosed());
+        loadImage(mediaResourceManager, "image1", 1 * KB, false /* shouldBeCached */, false);
+        assertEquals("image1", mImageResource.getImageId());
+    }
+
+    public void testLoadInvalidImage() {
+        final MediaResourceManager mediaResourceManager =
+                new MediaResourceManager();
+        MediaCacheManager.get().reclaim();
+        assertNotNull(mediaResourceManager);
+
+        // Test the failure case with invalid resource.
+        loadImage(mediaResourceManager, FakeImageRequest.INVALID_KEY, 1 * KB, false,
+                true /* shouldFail */);
+    }
+
+    public void testLoadImageSynchronously() {
+        final MediaResourceManager mediaResourceManager =
+                new MediaResourceManager();
+        MediaCacheManager.get().reclaim();
+        assertNotNull(mediaResourceManager);
+
+        // Test a normal sync load.
+        final FakeImageRequest request = new FakeImageRequest("image1", 1 * KB);
+        final FakeImageResource resource = mediaResourceManager.requestMediaResourceSync(request);
+        assertNotNull(resource);
+        assertFalse(resource.isClosed());
+        assertNotSame(0, resource.getRefCount());
+        resource.release();
+
+        // Test a failed sync load.
+        final FakeImageRequest invalidRequest =
+                new FakeImageRequest(FakeImageRequest.INVALID_KEY, 1 * KB);
+        assertNull(mediaResourceManager.requestMediaResourceSync(invalidRequest));
+    }
+
+    private void loadImage(final MediaResourceManager manager, final String key,
+            final int size, final boolean shouldBeCached, final boolean shouldFail) {
+        try {
+            final CountDownLatch signal = new CountDownLatch(1);
+            mImageRequest = AsyncMediaRequestWrapper.createWith(new FakeImageRequest(key, size),
+                    createAssertListener(shouldBeCached, shouldFail, signal));
+            mImageRequest.bind("1");
+            manager.requestMediaResourceAsync(mImageRequest);
+
+            // Wait for the asynchronous callback before proceeding.
+            signal.await();
+        } catch (final InterruptedException e) {
+            fail("Something interrupted the signal await.");
+        }
+    }
+
+    private MediaResourceLoadListener<FakeImageResource> createAssertListener(
+            final boolean shouldBeCached, final boolean shouldFail, final CountDownLatch signal) {
+        return new MediaResourceLoadListener<FakeImageResource>() {
+            @Override
+            public void onMediaResourceLoaded(final MediaRequest<FakeImageResource> request,
+                    final FakeImageResource resource, final boolean isCached) {
+                assertEquals(mImageRequest, request);
+                assertNotNull(resource);
+                assertFalse(resource.isClosed());
+                assertNotSame(0, resource.getRefCount());
+                assertFalse(shouldFail);
+                assertEquals(shouldBeCached, resource.getCached());
+                resource.setCached(true);
+                mImageResource = resource;
+                signal.countDown();
+            }
+
+            @Override
+            public void onMediaResourceLoadError(
+                    final MediaRequest<FakeImageResource> request, final Exception exception) {
+                assertTrue(shouldFail);
+                mImageResource = null;
+                signal.countDown();
+            }};
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/ActivityInstrumentationTestCaseIntent.java b/tests/src/com/android/messaging/ui/ActivityInstrumentationTestCaseIntent.java
new file mode 100644
index 0000000..5ea6aa7
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/ActivityInstrumentationTestCaseIntent.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+
+
+/**
+ * Purpose of this class is providing a workaround for https://b/14561718
+ */
+public class ActivityInstrumentationTestCaseIntent extends Intent {
+    public ActivityInstrumentationTestCaseIntent(Context packageContext, Class<?> cls) {
+        super(packageContext, cls);
+    }
+    @Override
+    public Intent setComponent(ComponentName component) {
+        // Ignore the ComponentName set, as the one ActivityUnitTest does is wrong (and actually
+        // unnecessary).
+        return this;
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/messaging/ui/BugleActivityInstrumentationTestCase.java b/tests/src/com/android/messaging/ui/BugleActivityInstrumentationTestCase.java
new file mode 100644
index 0000000..60cddff
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/BugleActivityInstrumentationTestCase.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import android.app.Activity;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.TestUtil;
+
+/**
+ * Helper class that extends ActivityInstrumentationTestCase2 to provide some extra common
+ * initialization (eg. Mockito boilerplate).
+ */
+public class BugleActivityInstrumentationTestCase<T extends Activity>
+    extends android.test.ActivityInstrumentationTestCase2<T> {
+
+    static {
+        // Set flag during loading of test cases to prevent application initialization starting
+        BugleTestCase.setTestsRunning();
+    }
+
+    public BugleActivityInstrumentationTestCase(final Class<T> activityClass) {
+        super(activityClass);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+      super.setUp();
+      setActivityInitialTouchMode(false);
+      TestUtil.testSetup(getInstrumentation().getTargetContext(), this);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        TestUtil.testTeardown(this);
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/messaging/ui/BugleActivityTest.java b/tests/src/com/android/messaging/ui/BugleActivityTest.java
new file mode 100644
index 0000000..05e32fa
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/BugleActivityTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.DataModel;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public abstract class BugleActivityTest extends BugleActivityUnitTestCase<BugleActionBarActivity> {
+    @Mock protected DataModel mDataModel;
+
+    public BugleActivityTest() {
+        super(BugleActionBarActivity.class);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // Create activity
+        final ActivityInstrumentationTestCaseIntent intent =
+                new ActivityInstrumentationTestCaseIntent(getInstrumentation().getTargetContext(),
+                TestActivity.class);
+        startActivity(intent, null, null);
+
+        FakeFactory.register(getInstrumentation().getTargetContext())
+            .withDataModel(mDataModel);
+    }
+
+    public void testOnResumeDataModelCallback() {
+        getInstrumentation().callActivityOnStart(getActivity());
+        getInstrumentation().callActivityOnResume(getActivity());
+        Mockito.verify(mDataModel).onActivityResume();
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/BugleActivityUnitTestCase.java b/tests/src/com/android/messaging/ui/BugleActivityUnitTestCase.java
new file mode 100644
index 0000000..dcbd785
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/BugleActivityUnitTestCase.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import android.app.Activity;
+import android.view.ContextThemeWrapper;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.R;
+import com.android.messaging.TestUtil;
+
+/**
+ * Base class for activity unit test cases, provides boilerplate setup/teardown.
+ */
+public abstract class BugleActivityUnitTestCase<T extends Activity> extends
+    android.test.ActivityUnitTestCase<T> {
+
+    static {
+        // Set flag during loading of test cases to prevent application initialization starting
+        BugleTestCase.setTestsRunning();
+    }
+
+    public BugleActivityUnitTestCase(final Class<T> activityClass) {
+        super(activityClass);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        TestUtil.testSetup(getInstrumentation().getTargetContext(), this);
+
+        setActivityContext(new ContextThemeWrapper(getInstrumentation().getTargetContext(),
+                R.style.Theme_AppCompat_Light_DarkActionBar));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        TestUtil.testTeardown(this);
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/CustomHeaderViewPagerTest.java b/tests/src/com/android/messaging/ui/CustomHeaderViewPagerTest.java
new file mode 100644
index 0000000..011cb82
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/CustomHeaderViewPagerTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import android.support.v4.view.ViewPager;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SimpleCursorAdapter;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+
+public class CustomHeaderViewPagerTest extends ViewTest<CustomHeaderViewPager> {
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getInstrumentation().getTargetContext());
+    }
+
+    public void testBindFirstLevel() {
+        final CustomHeaderViewPager view = new CustomHeaderViewPager(getActivity(), null);
+        final SimpleCursorAdapter adapter =
+                new SimpleCursorAdapter(getActivity(), 0, null, null, null, 0);
+        final CustomHeaderPagerViewHolder[] viewHolders = {
+                new FakeListViewHolder(getActivity(), adapter),
+                new FakeListViewHolder(getActivity(), adapter)
+        };
+
+        view.setViewHolders(viewHolders);
+        final ViewPager pager = (ViewPager) view.findViewById(R.id.pager);
+        final ViewGroup tabStrip = (ViewGroup) view.findViewById(R.id.tab_strip);
+        final ViewPagerTabStrip realTab = (ViewPagerTabStrip) tabStrip.getChildAt(0);
+
+        assertEquals(2, realTab.getChildCount());
+        View headerTitleButton = realTab.getChildAt(1);
+        // Click on the first page. Now the view pager should switch to that page accordingly.
+        clickButton(headerTitleButton);
+        assertEquals(1, pager.getCurrentItem());
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        // All set up should be done by creating a CustomHeaderViewPager which handles inflating
+        // the layout
+        return 0;
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/FakeListViewHolder.java b/tests/src/com/android/messaging/ui/FakeListViewHolder.java
new file mode 100644
index 0000000..d4de885
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/FakeListViewHolder.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import android.content.Context;
+import android.widget.CursorAdapter;
+
+import com.android.messaging.R;
+
+/**
+ * A fake {@link CustomHeaderPagerListViewHolder} for CustomHeaderViewPager tests only.
+ */
+public class FakeListViewHolder extends CustomHeaderPagerListViewHolder {
+    public FakeListViewHolder(final Context context, final CursorAdapter adapter) {
+        super(context, adapter);
+    }
+
+    @Override
+    protected int getLayoutResId() {
+        return 0;
+    }
+
+    @Override
+    protected int getPageTitleResId() {
+        return android.R.string.untitled;
+    }
+
+    @Override
+    protected int getEmptyViewResId() {
+        return R.id.empty_view;
+    }
+
+    @Override
+    protected int getListViewResId() {
+        return android.R.id.list;
+    }
+
+    @Override
+    protected int getEmptyViewTitleResId() {
+        return R.string.contact_list_empty_text;
+    }
+
+    @Override
+    protected int getEmptyViewImageResId() {
+        return R.drawable.ic_oobe_freq_list;
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/messaging/ui/FragmentTestCase.java b/tests/src/com/android/messaging/ui/FragmentTestCase.java
new file mode 100644
index 0000000..eb65dc6
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/FragmentTestCase.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.res.Configuration;
+import android.view.View;
+
+/**
+ * Helper class that extends Bugle.ui.ActivityInstrumentationTestCase to provide common behavior
+ * across fragment tests.
+ */
+public abstract class FragmentTestCase<T extends Fragment>
+    extends BugleActivityInstrumentationTestCase<TestActivity> {
+
+    protected T mFragment;
+    protected Class<T> mFragmentClass;
+
+    public FragmentTestCase(final Class<T> fragmentClass) {
+        super(TestActivity.class);
+        mFragmentClass = fragmentClass;
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+      super.setUp();
+    }
+
+    protected T getFragment() {
+        // Fragment creation deferred (typically until test time) so that factory/appcontext is
+        // ready.
+        if (mFragment == null) {
+            try {
+                mFragment = mFragmentClass.newInstance();
+            } catch (final InstantiationException e) {
+                throw new IllegalStateException("Failed to instantiate fragment");
+            } catch (final IllegalAccessException e) {
+                throw new IllegalStateException("Failed to instantiate fragment");
+            }
+        }
+
+        return mFragment;
+    }
+
+    protected void attachFragment() {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final FragmentManager fragmentManager = getActivity().getFragmentManager();
+                fragmentManager.beginTransaction().add(mFragment, null /* tag */).commit();
+            }
+        });
+
+        getInstrumentation().waitForIdleSync();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        // In landscape mode, sleep for a second first.
+        // The reason is: our UI tests don't wait for the UI thread to finish settling down
+        // before exiting (because they can't know when the UI thread is done). In portrait mode,
+        // things generally work fine here -- the UI thread is done by the time the test is done.
+        // In landscape mode, though, since the launcher is in portrait mode, there is a lot of
+        // extra work that happens in our UI when the app launches into landscape mode, and the
+        // UI is often not done by the time the test finishes running. So then our teardown
+        // nulls out the Factory, and then the UI keeps running and derefs the null factory,
+        // and things blow up.
+        // So ... as a cheap hack, sleep for one second before finishing the teardown of UI
+        // tests, but only do it in landscape mode (so that developers running it in portrait
+        // mode can still run the tests faster).
+        if (this.getInstrumentation().getTargetContext().getResources().
+                getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+        super.tearDown();
+    }
+
+    protected void clickButton(final View view) {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                view.performClick();
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+
+    protected void setFocus(final View view, final boolean focused) {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (focused) {
+                    view.requestFocus();
+                } else {
+                    view.clearFocus();
+                }
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/messaging/ui/MultiAttachmentLayoutTest.java b/tests/src/com/android/messaging/ui/MultiAttachmentLayoutTest.java
new file mode 100644
index 0000000..cf9a647
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/MultiAttachmentLayoutTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+
+import android.content.Context;
+import android.net.Uri;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.datamodel.data.MessagePartData;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+@MediumTest
+public class MultiAttachmentLayoutTest extends ViewTest<MultiAttachmentLayout> {
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final Context context = getInstrumentation().getTargetContext();
+        FakeFactory.register(context);
+    }
+
+    @Override
+    protected MultiAttachmentLayout getView() {
+        if (mView == null) {
+            // View creation deferred (typically until test time) so that factory/appcontext is
+            // ready.
+            mView = new MultiAttachmentLayout(getActivity(), null);
+            mView.setLayoutParams(new ViewGroup.LayoutParams(100, 100));
+        }
+        return mView;
+    }
+
+    protected void verifyContent(
+            final MultiAttachmentLayout view,
+            final int imageCount,
+            final int plusCount) {
+        final int count = view.getChildCount();
+        int actualImageCount = 0;
+        final boolean needPlusText = plusCount > 0;
+        boolean hasPlusText = false;
+        for (int i = 0; i < count; i++) {
+            final View child = view.getChildAt(i);
+            if (child instanceof AsyncImageView) {
+                actualImageCount++;
+            } else if (child instanceof TextView) {
+                assertTrue(plusCount > 0);
+                assertTrue(((TextView) child).getText().toString().contains("" + plusCount));
+                hasPlusText = true;
+            } else {
+                // Nothing other than image and overflow text view should appear in this layout.
+                fail("unexpected view in layout. view = " + child);
+            }
+        }
+        assertEquals(imageCount, actualImageCount);
+        assertEquals(needPlusText, hasPlusText);
+    }
+
+    public void testBindTwoAttachments() {
+        final MultiAttachmentLayout view = getView();
+        final MessagePartData testAttachment1 = MessagePartData.createMediaMessagePart(
+                "image/jpeg", Uri.parse("content://uri1"), 100, 100);
+        final MessagePartData testAttachment2 = MessagePartData.createMediaMessagePart(
+                "image/jpeg", Uri.parse("content://uri2"), 100, 100);
+
+        view.bindAttachments(createAttachmentList(testAttachment1, testAttachment2),
+                null /* transitionRect */, 2);
+        verifyContent(view, 2, 0);
+    }
+
+    public void testBindFiveAttachments() {
+        final MultiAttachmentLayout view = getView();
+        final MessagePartData testAttachment1 = MessagePartData.createMediaMessagePart(
+                "image/jpeg", Uri.parse("content://uri1"), 100, 100);
+        final MessagePartData testAttachment2 = MessagePartData.createMediaMessagePart(
+                "image/jpeg", Uri.parse("content://uri2"), 100, 100);
+        final MessagePartData testAttachment3 = MessagePartData.createMediaMessagePart(
+                "image/jpeg", Uri.parse("content://uri3"), 100, 100);
+        final MessagePartData testAttachment4 = MessagePartData.createMediaMessagePart(
+                "image/jpeg", Uri.parse("content://uri4"), 100, 100);
+        final MessagePartData testAttachment5 = MessagePartData.createMediaMessagePart(
+                "image/jpeg", Uri.parse("content://uri5"), 100, 100);
+
+        view.bindAttachments(createAttachmentList(testAttachment1, testAttachment2, testAttachment3,
+                testAttachment4, testAttachment5), null /* transitionRect */, 5);
+        verifyContent(view, 4, 1);
+    }
+
+    public void testBindTwice() {
+        // Put the above two tests together so we can simulate binding twice.
+        testBindTwoAttachments();
+        testBindFiveAttachments();
+    }
+
+    private Iterable<MessagePartData> createAttachmentList(final MessagePartData... attachments) {
+        return Collections.unmodifiableList(Arrays.asList(attachments));
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        return 0;   // We construct the view with getView().
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/messaging/ui/ViewTest.java b/tests/src/com/android/messaging/ui/ViewTest.java
new file mode 100644
index 0000000..c4e8431
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/ViewTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui;
+
+import android.view.View;
+
+
+/**
+ * Base class for view tests. Derived class just has to provide a layout id. Tests can then just
+ * call getView() to get a created view and test its behavior.
+ */
+public abstract class ViewTest<T extends View> extends BugleActivityUnitTestCase<TestActivity> {
+    public ViewTest() {
+        super(TestActivity.class);
+    }
+
+    protected T mView;
+
+    @SuppressWarnings("unchecked")
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // Create activity
+        final ActivityInstrumentationTestCaseIntent intent =
+                new ActivityInstrumentationTestCaseIntent(getInstrumentation().getTargetContext(),
+                TestActivity.class);
+        startActivity(intent, null, null);
+    }
+
+    @SuppressWarnings("unchecked")
+    protected T getView() {
+        if (mView == null) {
+            // View creation deferred (typically until test time) so that factory/appcontext is
+            // ready.
+            mView = (T) getActivity().getLayoutInflater().inflate(getLayoutIdForView(), null);
+        }
+        return mView;
+    }
+
+    protected void clickButton(final View view) {
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                view.performClick();
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+
+    protected abstract int getLayoutIdForView();
+}
diff --git a/tests/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserFragmentTest.java b/tests/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserFragmentTest.java
new file mode 100644
index 0000000..30c711b
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserFragmentTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.attachmentchooser;
+
+import android.app.Fragment;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.widget.CheckBox;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.data.DraftMessageData;
+import com.android.messaging.datamodel.data.MessageData;
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.ui.FragmentTestCase;
+import com.android.messaging.ui.TestActivity;
+import com.android.messaging.ui.TestActivity.FragmentEventListener;
+import com.android.messaging.ui.attachmentchooser.AttachmentChooserFragment;
+import com.android.messaging.ui.attachmentchooser.AttachmentGridItemView;
+import com.android.messaging.ui.attachmentchooser.AttachmentGridView;
+import com.android.messaging.ui.attachmentchooser.AttachmentChooserFragment.AttachmentChooserFragmentHost;
+import com.android.messaging.ui.conversationlist.ConversationListFragment;
+
+import org.mockito.ArgumentMatcher;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+
+/**
+ * Unit tests for {@link ConversationListFragment}.
+ */
+@LargeTest
+public class AttachmentChooserFragmentTest extends FragmentTestCase<AttachmentChooserFragment> {
+
+    @Mock protected DataModel mockDataModel;
+    @Mock protected DraftMessageData mockDraftMessageData;
+    @Mock protected AttachmentChooserFragmentHost mockHost;
+
+    private static final String CONVERSATION_ID = "cid";
+
+    /** A custom argument matcher that checks whether the set argument passed in is a set
+     * with identical attachment data as the given set.
+     */
+    private class IsSetOfGivenAttachments extends ArgumentMatcher<Set<MessagePartData>> {
+        private final Set<MessagePartData> mGivenParts;
+        public IsSetOfGivenAttachments(final Set<MessagePartData> givenParts) {
+            mGivenParts = givenParts;
+        }
+
+        @Override
+        public boolean matches(final Object set) {
+            @SuppressWarnings("unchecked")
+            final Set<MessagePartData> actualSet = (Set<MessagePartData>) set;
+            if (actualSet.size() != mGivenParts.size()) {
+                return false;
+            }
+            return mGivenParts.containsAll(actualSet) && actualSet.containsAll(mGivenParts);
+        }
+     }
+
+    public AttachmentChooserFragmentTest() {
+        super(AttachmentChooserFragment.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(this.getInstrumentation().getTargetContext())
+            .withDataModel(mockDataModel);
+    }
+
+    private void loadWith(final List<MessagePartData> attachments) {
+        Mockito.when(mockDraftMessageData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+        Mockito.doReturn(mockDraftMessageData)
+            .when(mockDataModel)
+            .createDraftMessageData(Mockito.anyString());
+        Mockito.doReturn(attachments)
+            .when(mockDraftMessageData)
+            .getReadOnlyAttachments();
+        Mockito.when(mockDataModel.createDraftMessageData(
+                Matchers.anyString()))
+            .thenReturn(mockDraftMessageData);
+
+        // Create fragment synchronously to avoid need for volatile, synchronization etc.
+        final AttachmentChooserFragment fragment = getFragment();
+        // Binding to model happens when attaching fragment to activity, so hook into test
+        // activity to do so.
+        getActivity().setFragmentEventListener(new FragmentEventListener() {
+            @Override
+            public void onAttachFragment(final Fragment attachedFragment) {
+                if (fragment == attachedFragment) {
+                    fragment.setConversationId(CONVERSATION_ID);
+                }
+            }
+        });
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setHost(mockHost);
+                getActivity().setFragment(fragment);
+                Mockito.verify(mockDataModel).createDraftMessageData(
+                        Mockito.matches(CONVERSATION_ID));
+                Mockito.verify(mockDraftMessageData).loadFromStorage(
+                        Matchers.eq(fragment.mBinding), Matchers.eq((MessageData) null),
+                        Matchers.eq(false));
+            }
+        });
+        // Now load the cursor
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                fragment.onDraftChanged(mockDraftMessageData, DraftMessageData.ALL_CHANGED);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+
+    public void testUnselect() {
+        final List<MessagePartData> attachments = TestDataFactory.getTestDraftAttachments();
+        loadWith(attachments);
+        final AttachmentGridView attachmentGridView = (AttachmentGridView)
+                getFragment().getView().findViewById(R.id.grid);
+        assertEquals("bad view count", attachments.size(),
+                attachmentGridView.getAdapter().getCount());
+
+        final AttachmentGridItemView itemView = (AttachmentGridItemView)
+                attachmentGridView.getChildAt(0);
+        assertEquals(attachmentGridView, itemView.testGetHostInterface());
+        final CheckBox checkBox = (CheckBox) itemView.findViewById(R.id.checkbox);
+        assertEquals(true, checkBox.isChecked());
+        assertEquals(true, attachmentGridView.isItemSelected(itemView.mAttachmentData));
+        clickButton(checkBox);
+        assertEquals(false, checkBox.isChecked());
+        assertEquals(false, attachmentGridView.isItemSelected(itemView.mAttachmentData));
+
+        final AttachmentGridItemView itemView2 = (AttachmentGridItemView)
+                attachmentGridView.getChildAt(1);
+        final CheckBox checkBox2 = (CheckBox) itemView2.findViewById(R.id.checkbox);
+        clickButton(checkBox2);
+
+        getFragment().confirmSelection();
+        final MessagePartData[] attachmentsToRemove = new MessagePartData[] {
+                itemView.mAttachmentData, itemView2.mAttachmentData };
+        Mockito.verify(mockDraftMessageData).removeExistingAttachments(Matchers.argThat(
+                new IsSetOfGivenAttachments(new HashSet<>(Arrays.asList(attachmentsToRemove)))));
+        Mockito.verify(mockDraftMessageData).saveToStorage(Matchers.eq(getFragment().mBinding));
+        Mockito.verify(mockHost).onConfirmSelection();
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/contact/ContactListItemViewTest.java b/tests/src/com/android/messaging/ui/contact/ContactListItemViewTest.java
new file mode 100644
index 0000000..de4c583
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/contact/ContactListItemViewTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.contact;
+
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.data.ContactListItemData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.ui.ContactIconView;
+import com.android.messaging.ui.ViewTest;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class ContactListItemViewTest extends ViewTest<ContactListItemView> {
+
+    @Mock ContactListItemView.HostInterface mockHost;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final Context context = getInstrumentation().getTargetContext();
+        FakeFactory.register(context)
+            .withDataModel(new FakeDataModel(context));
+    }
+
+    protected void verifyAddedContactForData(final ContactListItemData data,
+            final ContactListItemView view) {
+        Mockito.verify(mockHost).onContactListItemClicked(data, view);
+    }
+
+    protected void verifyContent(
+            final ContactListItemView view,
+            final String contactName,
+            final String contactDetail,
+            final String avatarUrl,
+            final boolean showAvatar) {
+        final TextView contactNameView = (TextView) view.findViewById(R.id.contact_name);
+        final TextView contactDetailView = (TextView) view.findViewById(R.id.contact_details);
+        final ContactIconView avatarView = (ContactIconView) view.findViewById(R.id.contact_icon);
+
+        assertNotNull(contactNameView);
+        assertEquals(contactName, contactNameView.getText());
+        assertNotNull(contactDetail);
+        assertEquals(contactDetail, contactDetailView.getText());
+        assertNotNull(avatarView);
+        if (showAvatar) {
+            assertTrue(avatarView.mImageRequestBinding.isBound());
+            assertEquals(View.VISIBLE, avatarView.getVisibility());
+        } else {
+            assertFalse(avatarView.mImageRequestBinding.isBound());
+            assertEquals(View.INVISIBLE, avatarView.getVisibility());
+        }
+    }
+
+    public void testBindFirstLevel() {
+        final ContactListItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getAllContactListCursor();
+        final int row = TestDataFactory.CONTACT_LIST_CURSOR_FIRST_LEVEL_CONTACT_INDEX;
+        cursor.moveToPosition(row);
+        view.bind(cursor, mockHost, false, null);
+        verifyContent(view, (String) cursor.getAt(Contacts.DISPLAY_NAME, row),
+                (String) cursor.getAt(Phone.NUMBER, row),
+                (String) cursor.getAt(Contacts.PHOTO_THUMBNAIL_URI, row), true);
+    }
+
+    public void testBindSecondLevel() {
+        final ContactListItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getAllContactListCursor();
+        final int row = TestDataFactory.CONTACT_LIST_CURSOR_SECOND_LEVEL_CONTACT_INDEX;
+        cursor.moveToPosition(row);
+        view.bind(cursor, mockHost, false, null);
+        verifyContent(view, (String) cursor.getAt(Contacts.DISPLAY_NAME, row),
+                (String) cursor.getAt(Phone.NUMBER, row),
+                (String) cursor.getAt(Contacts.PHOTO_THUMBNAIL_URI, row), false);
+    }
+
+    public void testClickAddedContact() {
+        final ContactListItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getAllContactListCursor();
+        cursor.moveToFirst();
+
+        view.bind(cursor, mockHost, false, null);
+        view.performClick();
+        verifyAddedContactForData(view.mData, view);
+    }
+
+    public void testBindTwice() {
+        final ContactListItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getAllContactListCursor();
+
+        cursor.moveToFirst();
+        view.bind(cursor, mockHost, false, null);
+
+        cursor.moveToNext();
+        view.bind(cursor, mockHost, false, null);
+        verifyContent(view, (String) cursor.getAt(Contacts.DISPLAY_NAME, 1),
+                (String) cursor.getAt(Phone.NUMBER, 1),
+                (String) cursor.getAt(Contacts.PHOTO_THUMBNAIL_URI, 1), true);
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        return R.layout.contact_list_item_view;
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/contact/ContactPickerFragmentTest.java b/tests/src/com/android/messaging/ui/contact/ContactPickerFragmentTest.java
new file mode 100644
index 0000000..5b1503b
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/contact/ContactPickerFragmentTest.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.contact;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.view.ViewPager;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+import android.widget.ListView;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.action.ActionTestHelpers;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubActionService;
+import com.android.messaging.datamodel.action.ActionTestHelpers.StubActionService.StubActionServiceCallLog;
+import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
+import com.android.messaging.datamodel.data.ContactPickerData;
+import com.android.messaging.datamodel.data.ParticipantData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.ui.CustomHeaderViewPagerAdapter;
+import com.android.messaging.ui.FragmentTestCase;
+import com.android.messaging.ui.UIIntents;
+import com.android.messaging.ui.contact.ContactPickerFragment.ContactPickerFragmentHost;
+
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Unit tests for {@link ContactPickerFragment}.
+ */
+@LargeTest
+public class ContactPickerFragmentTest
+    extends FragmentTestCase<ContactPickerFragment> {
+
+    @Mock protected ContactPickerData mMockContactPickerData;
+    @Mock protected UIIntents mMockUIIntents;
+    @Mock protected ContactPickerFragmentHost mockHost;
+    protected FakeDataModel mFakeDataModel;
+    private ActionTestHelpers.StubActionService mService;
+
+    public ContactPickerFragmentTest() {
+        super(ContactPickerFragment.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        final Context context = getInstrumentation().getTargetContext();
+        mService = new StubActionService();
+        mFakeDataModel = new FakeDataModel(context)
+            .withContactPickerData(mMockContactPickerData)
+            .withActionService(mService);
+        FakeFactory.register(context)
+                .withDataModel(mFakeDataModel)
+                .withUIIntents(mMockUIIntents);
+    }
+
+    /**
+     * Helper method to initialize the ContactPickerFragment and its data.
+     */
+    private ContactPickerFragmentTest initFragment(final int initialMode) {
+        Mockito.when(mMockContactPickerData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final ContactPickerFragment fragment = getFragment();
+                fragment.setHost(mockHost);
+                fragment.setContactPickingMode(initialMode, false);
+
+                getActivity().setFragment(fragment);
+                Mockito.verify(mMockContactPickerData).init(fragment.getLoaderManager(),
+                        fragment.mBinding);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+        return this;
+    }
+
+    /**
+     * Bind the datamodel with all contacts cursor to populate the all contacts list in the
+     * fragment.
+     */
+    private ContactPickerFragmentTest loadWithAllContactsCursor(final Cursor cursor) {
+        Mockito.when(mMockContactPickerData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                getFragment().onAllContactsCursorUpdated(cursor);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+        return this;
+    }
+
+    /**
+     * Bind the datamodel with frequent contacts cursor to populate the contacts list in the
+     * fragment.
+     */
+    private ContactPickerFragmentTest loadWithFrequentContactsCursor(final Cursor cursor) {
+        Mockito.when(mMockContactPickerData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                getFragment().onFrequentContactsCursorUpdated(cursor);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+        return this;
+    }
+
+    /**
+     * Test the initial state of the fragment before loading data.
+     */
+    public void testInitialState() {
+        initFragment(ContactPickerFragment.MODE_PICK_INITIAL_CONTACT);
+
+        // Make sure that the frequent contacts view is shown by default.
+        final ViewPager pager = (ViewPager) getFragment().getView().findViewById(R.id.pager);
+        final View currentPagedView = pager.getChildAt(pager.getCurrentItem());
+        final View frequentContactsView = ((CustomHeaderViewPagerAdapter) pager.getAdapter())
+                .getViewHolder(0).getView(null);
+        assertEquals(frequentContactsView, currentPagedView);
+    }
+
+    /**
+     * Verifies that list view gets correctly populated given a cursor.
+     */
+    public void testLoadAllContactsList() {
+        final Cursor cursor = TestDataFactory.getAllContactListCursor();
+        initFragment(ContactPickerFragment.MODE_PICK_INITIAL_CONTACT)
+                .loadWithAllContactsCursor(cursor);
+        final ListView listView = (ListView) getFragment().getView()
+                .findViewById(R.id.all_contacts_list);
+        assertEquals(cursor.getCount(), listView.getCount());
+    }
+
+    /**
+     * Verifies that list view gets correctly populated given a cursor.
+     */
+    public void testLoadFrequentContactsList() {
+        final Cursor cursor = TestDataFactory.getFrequentContactListCursor();
+        initFragment(ContactPickerFragment.MODE_PICK_INITIAL_CONTACT)
+                .loadWithFrequentContactsCursor(cursor);
+        final ListView listView = (ListView) getFragment().getView()
+                .findViewById(R.id.frequent_contacts_list);
+        assertEquals(cursor.getCount(), listView.getCount());
+    }
+
+    public void testPickInitialContact() {
+        final Cursor cursor = TestDataFactory.getFrequentContactListCursor();
+        initFragment(ContactPickerFragment.MODE_PICK_INITIAL_CONTACT)
+                .loadWithFrequentContactsCursor(cursor);
+        final ListView listView = (ListView) getFragment().getView()
+                .findViewById(R.id.frequent_contacts_list);
+        // Click on the first contact to add it.
+        final ContactListItemView cliv = (ContactListItemView) listView.getChildAt(0);
+        clickButton(cliv);
+        final ContactRecipientAutoCompleteView chipsView = (ContactRecipientAutoCompleteView)
+                getFragment().getView()
+                .findViewById(R.id.recipient_text_view);
+        // Verify the contact is added to the chips view.
+        final List<ParticipantData> participants =
+                chipsView.getRecipientParticipantDataForConversationCreation();
+        assertEquals(1, participants.size());
+        assertEquals(cliv.mData.getDestination(), participants.get(0).getSendDestination());
+        assertTrue(mService.getCalls().get(0).action instanceof GetOrCreateConversationAction);
+    }
+
+    public void testLeaveChipsMode() {
+        final Cursor cursor = TestDataFactory.getFrequentContactListCursor();
+        initFragment(ContactPickerFragment.MODE_CHIPS_ONLY)
+                .loadWithFrequentContactsCursor(cursor);
+        // Click on the add more participants button
+        // TODO: Figure out a way to click on the add more participants button now that
+        // it's part of the menu.
+        // final ImageButton AddMoreParticipantsButton = (ImageButton) getFragment().getView()
+        //         .findViewById(R.id.add_more_participants_button);
+        // clickButton(AddMoreParticipantsButton);
+        // Mockito.verify(mockHost).onInitiateAddMoreParticipants();
+    }
+
+    public void testPickMoreContacts() {
+        final Cursor cursor = TestDataFactory.getFrequentContactListCursor();
+        initFragment(ContactPickerFragment.MODE_PICK_MORE_CONTACTS)
+                .loadWithFrequentContactsCursor(cursor);
+        final ListView listView = (ListView) getFragment().getView()
+                .findViewById(R.id.frequent_contacts_list);
+        // Click on the first contact to add it.
+        final ContactListItemView cliv = (ContactListItemView) listView.getChildAt(0);
+        clickButton(cliv);
+        // Verify that we don't attempt to create a conversation right away.
+        assertEquals(0, mService.getCalls().size());
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/conversation/ComposeMessageViewTest.java b/tests/src/com/android/messaging/ui/conversation/ComposeMessageViewTest.java
new file mode 100644
index 0000000..2dd2a89
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/conversation/ComposeMessageViewTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.conversation;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+import android.widget.EditText;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.binding.Binding;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.datamodel.data.ConversationData;
+import com.android.messaging.datamodel.data.DraftMessageData;
+import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftForSendTask;
+import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftTaskCallback;
+import com.android.messaging.datamodel.data.MessageData;
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.ui.ViewTest;
+import com.android.messaging.ui.conversation.ComposeMessageView.IComposeMessageViewHost;
+import com.android.messaging.util.BugleGservices;
+import com.android.messaging.util.FakeMediaUtil;
+import com.android.messaging.util.ImeUtil;
+
+import org.mockito.ArgumentMatcher;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+@MediumTest
+public class ComposeMessageViewTest extends ViewTest<ComposeMessageView> {
+    private Context mContext;
+
+    @Mock protected DataModel mockDataModel;
+    @Mock protected DraftMessageData mockDraftMessageData;
+    @Mock protected BugleGservices mockBugleGservices;
+    @Mock protected ImeUtil mockImeUtil;
+    @Mock protected IComposeMessageViewHost mockIComposeMessageViewHost;
+    @Mock protected MediaPlayer mockMediaPlayer;
+    @Mock protected ConversationInputManager mockInputManager;
+    @Mock protected ConversationData mockConversationData;
+
+    Binding<ConversationData> mConversationBinding;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mContext = getInstrumentation().getTargetContext();
+        FakeFactory.register(mContext)
+                .withDataModel(mockDataModel)
+                .withBugleGservices(mockBugleGservices)
+                .withMediaUtil(new FakeMediaUtil(mockMediaPlayer));
+
+        Mockito.doReturn(true).when(mockConversationData).isBound(Mockito.anyString());
+        mConversationBinding = BindingBase.createBinding(this);
+        mConversationBinding.bind(mockConversationData);
+    }
+
+    @Override
+    protected ComposeMessageView getView() {
+        final ComposeMessageView view = super.getView();
+        view.setInputManager(mockInputManager);
+        view.setConversationDataModel(BindingBase.createBindingReference(mConversationBinding));
+        return view;
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        return R.layout.compose_message_view;
+    }
+
+    public void testSend() {
+        Mockito.when(mockDraftMessageData.getReadOnlyAttachments())
+                .thenReturn(Collections.unmodifiableList(new ArrayList<MessagePartData>()));
+        Mockito.when(mockDraftMessageData.getIsDefaultSmsApp()).thenReturn(true);
+        Mockito.when(mockIComposeMessageViewHost.isReadyForAction()).thenReturn(true);
+        final ComposeMessageView view = getView();
+
+        final MessageData message = MessageData.createDraftSmsMessage("fake_id", "just_a_self_id",
+                "Sample Message");
+
+        Mockito.when(mockDraftMessageData.isBound(Matchers.anyString()))
+                .thenReturn(true);
+        Mockito.when(mockDraftMessageData.getMessageText()).thenReturn(message.getMessageText());
+        Mockito.when(mockDraftMessageData.prepareMessageForSending(
+                Matchers.<BindingBase<DraftMessageData>>any()))
+                .thenReturn(message);
+        Mockito.when(mockDraftMessageData.hasPendingAttachments()).thenReturn(false);
+        Mockito.doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                // Synchronously pass the draft check and callback.
+                ((CheckDraftTaskCallback)invocation.getArguments()[2]).onDraftChecked(
+                        mockDraftMessageData, CheckDraftForSendTask.RESULT_PASSED);
+                return null;
+            }
+        }).when(mockDraftMessageData).checkDraftForAction(Mockito.anyBoolean(), Mockito.anyInt(),
+                Mockito.<CheckDraftTaskCallback>any(),
+                Mockito.<Binding<DraftMessageData>>any());
+
+        view.bind(mockDraftMessageData, mockIComposeMessageViewHost);
+
+        final EditText composeEditText = (EditText) view.findViewById(R.id.compose_message_text);
+        final View sendButton = view.findViewById(R.id.send_message_button);
+
+        view.requestDraftMessage(false);
+
+        Mockito.verify(mockDraftMessageData).loadFromStorage(Matchers.any(BindingBase.class),
+                Matchers.any(MessageData.class), Mockito.eq(false));
+
+        view.onDraftChanged(mockDraftMessageData, DraftMessageData.ALL_CHANGED);
+
+        assertEquals(message.getMessageText(), composeEditText.getText().toString());
+
+        sendButton.performClick();
+        Mockito.verify(mockIComposeMessageViewHost).sendMessage(
+                Mockito.argThat(new ArgumentMatcher<MessageData>() {
+                    @Override
+                    public boolean matches(final Object o) {
+                        assertEquals(message.getMessageText(), ((MessageData) o).getMessageText());
+                        return true;
+                    }
+                }));
+    }
+
+    public void testNotDefaultSms() {
+        Mockito.when(mockDraftMessageData.getReadOnlyAttachments())
+                .thenReturn(Collections.unmodifiableList(new ArrayList<MessagePartData>()));
+        Mockito.when(mockDraftMessageData.getIsDefaultSmsApp()).thenReturn(false);
+        Mockito.when(mockIComposeMessageViewHost.isReadyForAction()).thenReturn(false);
+        final ComposeMessageView view = getView();
+
+        final MessageData message = MessageData.createDraftSmsMessage("fake_id", "just_a_self_id",
+                "Sample Message");
+
+        Mockito.when(mockDraftMessageData.isBound(Matchers.anyString()))
+                .thenReturn(true);
+        Mockito.when(mockDraftMessageData.getMessageText()).thenReturn(message.getMessageText());
+        Mockito.when(mockDraftMessageData.prepareMessageForSending(
+                Matchers.<BindingBase<DraftMessageData>>any()))
+                .thenReturn(message);
+        Mockito.when(mockDraftMessageData.hasPendingAttachments()).thenReturn(false);
+
+        view.bind(mockDraftMessageData, mockIComposeMessageViewHost);
+
+        final EditText composeEditText = (EditText) view.findViewById(R.id.compose_message_text);
+        final View sendButton = view.findViewById(R.id.send_message_button);
+
+        view.requestDraftMessage(false);
+
+        Mockito.verify(mockDraftMessageData).loadFromStorage(Matchers.any(BindingBase.class),
+                Matchers.any(MessageData.class), Mockito.eq(false));
+
+        view.onDraftChanged(mockDraftMessageData, DraftMessageData.ALL_CHANGED);
+
+        assertEquals(message.getMessageText(), composeEditText.getText().toString());
+
+        sendButton.performClick();
+        Mockito.verify(mockIComposeMessageViewHost).warnOfMissingActionConditions(
+                Matchers.any(Boolean.class), Matchers.any(Runnable.class));
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/conversation/ConversationActivityUiStateTest.java b/tests/src/com/android/messaging/ui/conversation/ConversationActivityUiStateTest.java
new file mode 100644
index 0000000..7c6903d
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/conversation/ConversationActivityUiStateTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.conversation;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.ui.contact.ContactPickerFragment;
+import com.android.messaging.ui.conversation.ConversationActivityUiState;
+import com.android.messaging.ui.conversation.ConversationActivityUiState.ConversationActivityUiStateHost;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+@SmallTest
+public class ConversationActivityUiStateTest extends BugleTestCase {
+    @Mock protected ConversationActivityUiStateHost mockListener;
+
+    /**
+     * Test the Ui state where we start off with the contact picker to pick the first contact.
+     */
+    public void testPickInitialContact() {
+        final ConversationActivityUiState uiState = new ConversationActivityUiState(null);
+        uiState.setHost(mockListener);
+        assertTrue(uiState.shouldShowContactPickerFragment());
+        assertFalse(uiState.shouldShowConversationFragment());
+        assertEquals(ContactPickerFragment.MODE_PICK_INITIAL_CONTACT,
+                uiState.getDesiredContactPickingMode());
+        uiState.onGetOrCreateConversation("conversation1");
+        Mockito.verify(mockListener, Mockito.times(1)).onConversationContactPickerUiStateChanged(
+                Mockito.eq(ConversationActivityUiState.STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT),
+                Mockito.eq(
+                        ConversationActivityUiState.STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW),
+                Mockito.anyBoolean());
+        assertTrue(uiState.shouldShowContactPickerFragment());
+        assertTrue(uiState.shouldShowConversationFragment());
+        assertTrue(TextUtils.equals("conversation1", uiState.getConversationId()));
+        assertEquals(ContactPickerFragment.MODE_CHIPS_ONLY,
+                uiState.getDesiredContactPickingMode());
+    }
+
+    /**
+     * Test the Ui state where we have both the chips view and the conversation view and we
+     * start message compose.
+     */
+    public void testHybridUiStateStartCompose() {
+        final ConversationActivityUiState uiState = new ConversationActivityUiState("conv1");
+        uiState.testSetUiState(
+                ConversationActivityUiState.STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW);
+        uiState.setHost(mockListener);
+
+        // Start message compose.
+        uiState.onStartMessageCompose();
+        Mockito.verify(mockListener, Mockito.times(1)).onConversationContactPickerUiStateChanged(
+                Mockito.eq(
+                        ConversationActivityUiState.STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW),
+                Mockito.eq(ConversationActivityUiState.STATE_CONVERSATION_ONLY),
+                Mockito.anyBoolean());
+        assertFalse(uiState.shouldShowContactPickerFragment());
+        assertTrue(uiState.shouldShowConversationFragment());
+    }
+
+    /**
+     * Test the Ui state where we have both the chips view and the conversation view and we
+     * try to add a participant.
+     */
+    public void testHybridUiStateAddParticipant() {
+        final ConversationActivityUiState uiState = new ConversationActivityUiState("conv1");
+        uiState.testSetUiState(
+                ConversationActivityUiState.STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW);
+        uiState.setHost(mockListener);
+
+        uiState.onAddMoreParticipants();
+        Mockito.verify(mockListener, Mockito.times(1)).onConversationContactPickerUiStateChanged(
+                Mockito.eq(
+                        ConversationActivityUiState.STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW),
+                Mockito.eq(
+                        ConversationActivityUiState.STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS),
+                Mockito.anyBoolean());
+        assertTrue(uiState.shouldShowContactPickerFragment());
+        assertFalse(uiState.shouldShowConversationFragment());
+        assertEquals(ContactPickerFragment.MODE_PICK_MORE_CONTACTS,
+                uiState.getDesiredContactPickingMode());
+    }
+
+    /**
+     * Test the Ui state where we are trying to add more participants and commit.
+     */
+    public void testCommitAddParticipant() {
+        final ConversationActivityUiState uiState = new ConversationActivityUiState(null);
+        uiState.testSetUiState(
+                ConversationActivityUiState.STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS);
+        uiState.setHost(mockListener);
+
+        uiState.onGetOrCreateConversation("conversation1");
+
+        // After adding more contacts, the terminal state is always conversation only (i.e. we
+        // don't go back to hybrid mode).
+        Mockito.verify(mockListener, Mockito.times(1)).onConversationContactPickerUiStateChanged(
+                Mockito.eq(ConversationActivityUiState.STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS),
+                Mockito.eq(ConversationActivityUiState.STATE_CONVERSATION_ONLY),
+                Mockito.anyBoolean());
+        assertFalse(uiState.shouldShowContactPickerFragment());
+        assertTrue(uiState.shouldShowConversationFragment());
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/conversation/ConversationFragmentTest.java b/tests/src/com/android/messaging/ui/conversation/ConversationFragmentTest.java
new file mode 100644
index 0000000..c92fbf6
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/conversation/ConversationFragmentTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.conversation;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.database.Cursor;
+import android.support.v7.widget.RecyclerView;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.MemoryCacheManager;
+import com.android.messaging.datamodel.data.ConversationData;
+import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
+import com.android.messaging.datamodel.data.DraftMessageData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.datamodel.media.MediaResourceManager;
+import com.android.messaging.ui.FragmentTestCase;
+import com.android.messaging.ui.PlainTextEditText;
+import com.android.messaging.ui.TestActivity.FragmentEventListener;
+import com.android.messaging.ui.conversation.ConversationFragment.ConversationFragmentHost;
+import com.android.messaging.ui.conversationlist.ConversationListFragment;
+import com.android.messaging.util.BugleGservices;
+import com.android.messaging.util.ImeUtil;
+
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+
+/**
+ * Unit tests for {@link ConversationListFragment}.
+ */
+@LargeTest
+public class ConversationFragmentTest extends FragmentTestCase<ConversationFragment> {
+
+    @Mock protected DataModel mockDataModel;
+    @Mock protected ConversationData mockConversationData;
+    @Mock protected DraftMessageData mockDraftMessageData;
+    @Mock protected MediaResourceManager mockMediaResourceManager;
+    @Mock protected BugleGservices mockBugleGservices;
+    @Mock protected ConversationFragmentHost mockHost;
+    @Mock protected MemoryCacheManager mockMemoryCacheManager;
+
+    private ImeUtil mSpiedImeUtil;
+
+    private static final String CONVERSATION_ID = "cid";
+
+
+    public ConversationFragmentTest() {
+        super(ConversationFragment.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+      super.setUp();
+      ImeUtil.clearInstance();
+      mSpiedImeUtil = Mockito.spy(new ImeUtil());
+      FakeFactory.register(this.getInstrumentation().getTargetContext())
+          .withDataModel(mockDataModel)
+          .withBugleGservices(mockBugleGservices)
+          .withMemoryCacheManager(mockMemoryCacheManager);
+    }
+
+    /**
+     * Helper that will do the 'binding' of ConversationFragmentTest with ConversationData and
+     * leave fragment in 'ready' state.
+     * @param cursor
+     */
+    private void loadWith(final Cursor cursor) {
+        Mockito.when(mockDraftMessageData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+        Mockito.when(mockConversationData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+        Mockito.doReturn(mockDraftMessageData)
+            .when(mockDataModel)
+            .createDraftMessageData(Mockito.anyString());
+        Mockito.when(mockDataModel.createConversationData(
+                Matchers.any(Activity.class),
+                Matchers.any(ConversationDataListener.class),
+                Matchers.anyString()))
+            .thenReturn(mockConversationData);
+
+        // Create fragment synchronously to avoid need for volatile, synchronization etc.
+        final ConversationFragment fragment = getFragment();
+        // Binding to model happens when attaching fragment to activity, so hook into test
+        // activity to do so.
+        getActivity().setFragmentEventListener(new FragmentEventListener() {
+            @Override
+            public void onAttachFragment(final Fragment attachedFragment) {
+                if (fragment == attachedFragment) {
+                    fragment.setConversationInfo(getActivity(), CONVERSATION_ID, null);
+                }
+            }
+        });
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setHost(mockHost);
+                getActivity().setFragment(fragment);
+                Mockito.verify(mockDataModel).createConversationData(
+                        getActivity(), fragment, CONVERSATION_ID);
+                Mockito.verify(mockConversationData).init(fragment.getLoaderManager(),
+                        fragment.mBinding);
+            }
+        });
+        // Wait for initial layout pass to work around crash in recycler view
+        getInstrumentation().waitForIdleSync();
+        // Now load the cursor
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                fragment.onConversationMessagesCursorUpdated(mockConversationData, cursor, null,
+                        false);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+
+    /**
+     * Verifies that list view gets correctly populated given a cursor.
+     */
+    public void testLoadListView() {
+        final Cursor cursor = TestDataFactory.getConversationMessageCursor();
+        loadWith(cursor);
+        final RecyclerView listView =
+                (RecyclerView) getFragment().getView().findViewById(android.R.id.list);
+        assertEquals("bad cursor", cursor.getCount(), listView.getAdapter().getItemCount());
+        assertEquals("bad cursor count", cursor.getCount(), listView.getChildCount());
+    }
+
+    public void testClickComposeMessageView() {
+        final Cursor cursor = TestDataFactory.getConversationMessageCursor();
+        loadWith(cursor);
+
+        final PlainTextEditText composeEditText = (PlainTextEditText) getFragment().getView()
+                .findViewById(R.id.compose_message_text);
+        setFocus(composeEditText, false);
+        Mockito.verify(mockHost, Mockito.never()).onStartComposeMessage();
+        setFocus(composeEditText, true);
+        Mockito.verify(mockHost).onStartComposeMessage();
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/conversation/ConversationInputManagerTest.java b/tests/src/com/android/messaging/ui/conversation/ConversationInputManagerTest.java
new file mode 100644
index 0000000..f335785
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/conversation/ConversationInputManagerTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.conversation;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.os.Bundle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.widget.EditText;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.binding.Binding;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.datamodel.data.ConversationData;
+import com.android.messaging.datamodel.data.DraftMessageData;
+import com.android.messaging.datamodel.data.SubscriptionListData;
+import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputHost;
+import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
+import com.android.messaging.ui.mediapicker.MediaPicker;
+import com.android.messaging.util.BugleGservices;
+import com.android.messaging.util.ImeUtil;
+import com.android.messaging.util.ImeUtil.ImeStateHost;
+
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+
+@SmallTest
+public class ConversationInputManagerTest extends BugleTestCase {
+    @Spy protected ImeUtil spyImeUtil;
+    @Mock protected BugleGservices mockBugleGservices;
+    @Mock protected FragmentManager mockFragmentManager;
+    @Mock protected ConversationInputHost mockConversationInputHost;
+    @Mock protected ConversationInputSink mockConversationInputSink;
+    @Mock protected ImeStateHost mockImeStateHost;
+    @Mock protected ConversationData mockConversationData;
+    @Mock protected DraftMessageData mockDraftMessageData;
+    @Mock protected MediaPicker mockMediaPicker;
+    @Mock protected SubscriptionListData mockSubscriptionListData;
+    @Mock protected FragmentTransaction mockFragmentTransaction;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getTestContext())
+                .withBugleGservices(mockBugleGservices);
+        spyImeUtil = Mockito.spy(new ImeUtil());
+        ImeUtil.set(spyImeUtil);
+    }
+
+    private ConversationInputManager initNewInputManager(final Bundle savedState) {
+        // Set up the mocks.
+        Mockito.when(mockConversationInputHost.getSimSelectorView())
+                .thenReturn(new SimSelectorView(getTestContext(), null));
+        Mockito.when(mockConversationInputHost.createMediaPicker()).thenReturn(mockMediaPicker);
+        Mockito.when(mockConversationInputSink.getComposeEditText())
+                .thenReturn(new EditText(getTestContext()));
+        Mockito.doReturn(mockFragmentTransaction).when(mockFragmentTransaction).replace(
+                Mockito.eq(R.id.mediapicker_container), Mockito.any(MediaPicker.class),
+                Mockito.anyString());
+        Mockito.when(mockFragmentManager.findFragmentByTag(MediaPicker.FRAGMENT_TAG))
+                .thenReturn(null);
+        Mockito.when(mockFragmentManager.beginTransaction()).thenReturn(mockFragmentTransaction);
+        Mockito.when(mockSubscriptionListData.hasData()).thenReturn(true);
+        Mockito.when(mockConversationData.getSubscriptionListData())
+                .thenReturn(mockSubscriptionListData);
+        Mockito.doReturn(true).when(mockConversationData).isBound(Mockito.anyString());
+        Mockito.doReturn(true).when(mockDraftMessageData).isBound(Mockito.anyString());
+
+        final Binding<ConversationData> dataBinding = BindingBase.createBinding(this);
+        dataBinding.bind(mockConversationData);
+        final Binding<DraftMessageData> draftBinding = BindingBase.createBinding(this);
+        draftBinding.bind(mockDraftMessageData);
+        final ConversationInputManager inputManager = new ConversationInputManager(getTestContext(),
+                mockConversationInputHost, mockConversationInputSink, mockImeStateHost,
+                mockFragmentManager, dataBinding, draftBinding, savedState);
+        return inputManager;
+    }
+
+    public void testShowHideInputs() {
+        final ConversationInputManager inputManager = initNewInputManager(new Bundle());
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(true);
+        inputManager.showHideMediaPicker(true /* show */, true /* animate */);
+        Mockito.verify(mockFragmentTransaction).replace(
+                Mockito.eq(R.id.mediapicker_container), Mockito.any(MediaPicker.class),
+                Mockito.anyString());
+        Mockito.verify(mockMediaPicker).open(Mockito.anyInt(), Mockito.eq(true /* animate */));
+
+        assertEquals(true, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(false, inputManager.isImeKeyboardVisible());
+
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(false);
+        inputManager.showHideMediaPicker(false /* show */, true /* animate */);
+        Mockito.verify(mockMediaPicker).dismiss(Mockito.eq(true /* animate */));
+
+        assertEquals(false, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(false, inputManager.isImeKeyboardVisible());
+    }
+
+    public void testShowTwoInputsSequentially() {
+        // First show the media picker, then show the IME keyboard.
+        final ConversationInputManager inputManager = initNewInputManager(new Bundle());
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(true);
+        inputManager.showHideMediaPicker(true /* show */, true /* animate */);
+
+        assertEquals(true, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(false, inputManager.isImeKeyboardVisible());
+
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(false);
+        inputManager.showHideImeKeyboard(true /* show */, true /* animate */);
+
+        assertEquals(false, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(true, inputManager.isImeKeyboardVisible());
+    }
+
+    public void testOnKeyboardShow() {
+        final ConversationInputManager inputManager = initNewInputManager(new Bundle());
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(true);
+        inputManager.showHideMediaPicker(true /* show */, true /* animate */);
+
+        assertEquals(true, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(false, inputManager.isImeKeyboardVisible());
+
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(false);
+        inputManager.testNotifyImeStateChanged(true /* imeOpen */);
+
+        assertEquals(false, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(true, inputManager.isImeKeyboardVisible());
+    }
+
+    public void testRestoreState() {
+        final ConversationInputManager inputManager = initNewInputManager(new Bundle());
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(true);
+        inputManager.showHideMediaPicker(true /* show */, true /* animate */);
+
+        assertEquals(true, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(false, inputManager.isImeKeyboardVisible());
+
+        Bundle savedInstanceState = new Bundle();
+        inputManager.onSaveInputState(savedInstanceState);
+
+        // Now try to restore the state
+        final ConversationInputManager restoredInputManager =
+                initNewInputManager(savedInstanceState);
+
+        // Make sure the state is preserved.
+        assertEquals(true, restoredInputManager.isMediaPickerVisible());
+        assertEquals(false, restoredInputManager.isSimSelectorVisible());
+        assertEquals(false, restoredInputManager.isImeKeyboardVisible());
+    }
+
+    public void testBackPress() {
+        final ConversationInputManager inputManager = initNewInputManager(new Bundle());
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(true);
+        inputManager.showHideMediaPicker(true /* show */, true /* animate */);
+
+        assertEquals(true, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(false, inputManager.isImeKeyboardVisible());
+
+        Mockito.when(mockMediaPicker.isOpen()).thenReturn(false);
+        assertEquals(true, inputManager.onBackPressed());
+
+        assertEquals(false, inputManager.isMediaPickerVisible());
+        assertEquals(false, inputManager.isSimSelectorVisible());
+        assertEquals(false, inputManager.isImeKeyboardVisible());
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/conversation/ConversationMessageViewTest.java b/tests/src/com/android/messaging/ui/conversation/ConversationMessageViewTest.java
new file mode 100644
index 0000000..6b8b0c0
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/conversation/ConversationMessageViewTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.conversation;
+
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.ui.ViewTest;
+import com.android.messaging.ui.conversation.ConversationMessageView;
+import com.android.messaging.ui.conversation.ConversationMessageView.ConversationMessageViewHost;
+import com.android.messaging.util.Dates;
+
+import org.mockito.Mock;
+
+@MediumTest
+public class ConversationMessageViewTest extends ViewTest<ConversationMessageView> {
+    @Mock ConversationMessageViewHost mockHost;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getInstrumentation().getTargetContext());
+    }
+
+    @Override
+    protected ConversationMessageView getView() {
+        final ConversationMessageView view = super.getView();
+        view.setHost(mockHost);
+        return view;
+    }
+
+    protected void verifyContent(final ConversationMessageView view, final String messageText,
+            final boolean showTimestamp, final String timestampText) {
+
+        final TextView messageTextView = (TextView) view.findViewById(R.id.message_text);
+        final TextView statusTextView = (TextView) view.findViewById(R.id.message_status);
+
+        assertNotNull(messageTextView);
+        assertEquals(messageText, messageTextView.getText());
+
+        if (showTimestamp) {
+            assertEquals(View.VISIBLE, statusTextView.getVisibility());
+            assertEquals(timestampText, statusTextView.getText());
+        } else {
+            assertEquals(View.GONE, statusTextView.getVisibility());
+        }
+    }
+
+    public void testBind() {
+        final ConversationMessageView view = getView();
+
+        final FakeCursor cursor = TestDataFactory.getConversationMessageCursor();
+        cursor.moveToFirst();
+
+        view.bind(cursor);
+        verifyContent(view, TestDataFactory.getMessageText(cursor, 0), true, Dates
+                .getMessageTimeString((Long) cursor.getAt("received_timestamp", 0)).toString());
+    }
+
+    public void testBindTwice() {
+        final ConversationMessageView view = getView();
+
+        final FakeCursor cursor = TestDataFactory.getConversationMessageCursor();
+        cursor.moveToFirst();
+        view.bind(cursor);
+
+        cursor.moveToNext();
+        view.bind(cursor);
+        verifyContent(view, TestDataFactory.getMessageText(cursor, 1), true, Dates
+                .getMessageTimeString((Long) cursor.getAt("received_timestamp", 1)).toString());
+    }
+
+    public void testBindLast() {
+        final ConversationMessageView view = getView();
+
+        final FakeCursor cursor = TestDataFactory.getConversationMessageCursor();
+        final int lastPos = cursor.getCount() - 1;
+        cursor.moveToPosition(lastPos);
+
+        view.bind(cursor);
+        verifyContent(view, TestDataFactory.getMessageText(cursor, lastPos), true, Dates
+                .getMessageTimeString((Long) cursor.getAt("received_timestamp", lastPos))
+                .toString());
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        return R.layout.conversation_message_view;
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/conversationlist/ConversationListFragmentTest.java b/tests/src/com/android/messaging/ui/conversationlist/ConversationListFragmentTest.java
new file mode 100644
index 0000000..f9cc9e1
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/conversationlist/ConversationListFragmentTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.conversationlist;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v7.widget.RecyclerView;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.ListView;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.data.ConversationListData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.ui.FragmentTestCase;
+import com.android.messaging.ui.UIIntents;
+import com.android.messaging.ui.conversationlist.ConversationListFragment;
+import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost;
+
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+
+/**
+ * Unit tests for {@link ConversationListFragment}.
+ */
+@LargeTest
+public class ConversationListFragmentTest
+    extends FragmentTestCase<ConversationListFragment> {
+
+    @Mock protected ConversationListData mMockConversationListData;
+    @Mock protected ConversationListFragmentHost mMockConversationHostListHost;
+    @Mock protected UIIntents mMockUIIntents;
+    protected FakeDataModel mFakeDataModel;
+
+    public ConversationListFragmentTest() {
+        super(ConversationListFragment.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        final Context context = getInstrumentation().getTargetContext();
+        mFakeDataModel = new FakeDataModel(context)
+            .withConversationListData(mMockConversationListData);
+        FakeFactory.register(context)
+                .withDataModel(mFakeDataModel)
+                .withUIIntents(mMockUIIntents);
+    }
+
+    /**
+     * Helper that will do the 'binding' of ConversationListFragmentTest with ConversationListData
+     * and leave fragment in 'ready' state.
+     * @param cursor
+     */
+    private void loadWith(final Cursor cursor) {
+        Mockito.when(mMockConversationListData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+
+        final ConversationListFragment fragment = getFragment();
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                fragment.setHost(mMockConversationHostListHost);
+                getActivity().setFragment(fragment);
+                Mockito.verify(mMockConversationListData).init(fragment.getLoaderManager(),
+                        fragment.mListBinding);
+                fragment.onConversationListCursorUpdated(mMockConversationListData, cursor);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+
+    /**
+     * Verifies that list view gets correctly populated given a cursor.
+     */
+    public void testLoadListView() {
+        final Cursor cursor = TestDataFactory.getConversationListCursor();
+        loadWith(cursor);
+        final RecyclerView listView =
+                (RecyclerView) getFragment().getView().findViewById(android.R.id.list);
+        //assertEquals(cursor.getCount(), listView.getCount());
+        assertEquals(cursor.getCount(), listView.getChildCount());
+    }
+
+    /**
+     * Verifies that 'empty list' promo is rendered with an empty cursor.
+     */
+    public void testEmptyView() {
+        loadWith(TestDataFactory.getEmptyConversationListCursor());
+        final RecyclerView listView =
+                (RecyclerView) getFragment().getView().findViewById(android.R.id.list);
+        final View emptyMessageView =
+                getFragment().getView().findViewById(R.id.no_conversations_view);
+        assertEquals(View.VISIBLE, emptyMessageView.getVisibility());
+        assertEquals(0, listView.getChildCount());
+    }
+
+    /**
+     * Verifies that the button to start a new conversation works.
+     */
+    public void testStartNewConversation() {
+        final Cursor cursor = TestDataFactory.getConversationListCursor();
+        loadWith(cursor);
+        final ImageView startNewConversationButton = (ImageView)
+                getFragment().getView().findViewById(R.id.start_new_conversation_button);
+
+        clickButton(startNewConversationButton);
+        Mockito.verify(mMockConversationHostListHost).onCreateConversationClick();
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/conversationlist/ConversationListItemViewTest.java b/tests/src/com/android/messaging/ui/conversationlist/ConversationListItemViewTest.java
new file mode 100644
index 0000000..be054a8
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/conversationlist/ConversationListItemViewTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.conversationlist;
+
+import android.content.Context;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.messaging.Factory;
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.data.ConversationListItemData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.ui.AsyncImageView;
+import com.android.messaging.ui.UIIntentsImpl;
+import com.android.messaging.ui.ViewTest;
+import com.android.messaging.ui.conversationlist.ConversationListItemView;
+import com.android.messaging.util.Dates;
+
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+@MediumTest
+public class ConversationListItemViewTest extends ViewTest<ConversationListItemView> {
+
+    @Mock private ConversationListItemView.HostInterface mockHost;
+    private FakeCursor mCursor;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final Context context = getInstrumentation().getTargetContext();
+        FakeFactory.register(context)
+            .withDataModel(new FakeDataModel(context))
+            .withUIIntents(new UIIntentsImpl());
+        mCursor = TestDataFactory.getConversationListCursor();
+    }
+
+
+    protected void verifyLaunchedConversationForId(final String id,
+            final ConversationListItemView conversationView) {
+        // Must be a short click.
+        final ArgumentMatcher<ConversationListItemData> itemDataIdMatcher =
+                new ArgumentMatcher<ConversationListItemData>() {
+            @Override
+            public boolean matches(final Object arg) {
+                return TextUtils.equals(id, ((ConversationListItemData) arg).getConversationId());
+            }
+        };
+        Mockito.verify(mockHost).onConversationClicked(
+                Mockito.argThat(itemDataIdMatcher), Mockito.eq(false),
+                Mockito.eq(conversationView));
+    }
+
+    protected void verifyContent(
+            final ConversationListItemView view, final FakeCursor cursor, final int index) {
+        /* ConversationQueryColumns.NAME */
+        final String  conversationQueryColumnsName = "name";
+        final String name = (String) cursor.getAt(conversationQueryColumnsName, index);
+
+        /* ConversationQueryColumns.SNIPPET_TEXT */
+        final String  conversationQueryColumnsSnippetText = "snippet_text";
+        final String snippet = (String) cursor.getAt(conversationQueryColumnsSnippetText, index);
+
+        /* ConversationQueryColumns.SORT_TIMESTAMP */
+        final String  conversationQueryColumnsSortTimestamp = "sort_timestamp";
+        final String timestamp = Dates.getConversationTimeString(
+                (Long) cursor.getAt(conversationQueryColumnsSortTimestamp, index)).toString();
+
+        final boolean unread = !isRead(cursor, index);
+        verifyContent(view, name,  snippet, timestamp, unread);
+    }
+
+    protected void verifyContent(
+            final ConversationListItemView view,
+            final String conversationName,
+            final String snippet,
+            final String timestamp,
+            final boolean unread) {
+        final TextView conversationNameView =
+                (TextView) view.findViewById(R.id.conversation_name);
+        final TextView snippetTextView = (TextView) view.findViewById(R.id.conversation_snippet);
+        final TextView timestampTextView = (TextView) view.findViewById(
+                R.id.conversation_timestamp);
+        final AsyncImageView imagePreviewView =
+                (AsyncImageView) view.findViewById(R.id.conversation_image_preview);
+
+        final Context context = Factory.get().getApplicationContext();
+        assertNotNull(conversationNameView);
+        assertEquals(conversationName, conversationNameView.getText());
+        assertNotNull(snippetTextView);
+        if (unread) {
+            assertEquals(ConversationListItemView.UNREAD_SNIPPET_LINE_COUNT,
+                    snippetTextView.getMaxLines());
+            assertEquals(context.getResources().getColor(R.color.conversation_list_item_unread),
+                    snippetTextView.getCurrentTextColor());
+            assertEquals(context.getResources().getColor(R.color.conversation_list_item_unread),
+                    conversationNameView.getCurrentTextColor());
+
+        } else {
+            assertEquals(ConversationListItemView.NO_UNREAD_SNIPPET_LINE_COUNT,
+                    snippetTextView.getMaxLines());
+            assertEquals(context.getResources().getColor(R.color.conversation_list_item_read),
+                    snippetTextView.getCurrentTextColor());
+            assertEquals(context.getResources().getColor(R.color.conversation_list_item_read),
+                    conversationNameView.getCurrentTextColor());
+        }
+
+        assertEquals(View.VISIBLE, imagePreviewView.getVisibility());
+        assertTrue(snippetTextView.getText().toString().contains(snippet));
+        assertEquals(timestamp, timestampTextView.getText());
+    }
+
+    protected boolean isRead(final FakeCursor cursor, final int index) {
+        return 1 == ((Integer) cursor.getAt("read", index)).intValue();
+    }
+
+    public void testBindUnread() {
+        final ConversationListItemView view = getView();
+        final int messageIndex = TestDataFactory.CONVERSATION_LIST_CURSOR_UNREAD_MESSAGE_INDEX;
+        mCursor.moveToPosition(messageIndex);
+        assertFalse(isRead(mCursor, messageIndex));
+        view.bind(mCursor, mockHost);
+        verifyContent(view, mCursor, messageIndex);
+    }
+
+    public void testBindRead() {
+        final ConversationListItemView view = getView();
+
+        final int messageIndex = TestDataFactory.CONVERSATION_LIST_CURSOR_READ_MESSAGE_INDEX;
+        mCursor.moveToPosition(messageIndex);
+        assertTrue(isRead(mCursor, messageIndex));
+        view.bind(mCursor, mockHost);
+        verifyContent(view, mCursor, messageIndex);
+    }
+
+    public void testClickLaunchesConversation() {
+        final ConversationListItemView view = getView();
+        final View swipeableContainer = view.findViewById(R.id.swipeableContainer);
+        mCursor.moveToFirst();
+        view.bind(mCursor, mockHost);
+        swipeableContainer.performClick();
+        verifyLaunchedConversationForId(
+                mCursor.getAt("_id" /* ConversationQueryColumns._ID */, 0).toString(), view);
+    }
+
+    public void testBindTwice() {
+        final ConversationListItemView view = getView();
+
+        mCursor.moveToFirst();
+        view.bind(mCursor, mockHost);
+
+        mCursor.moveToNext();
+        view.bind(mCursor, mockHost);
+        verifyContent(view, mCursor, mCursor.getPosition());
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        return R.layout.conversation_list_item_view;
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/mediapicker/AudioRecordViewTest.java b/tests/src/com/android/messaging/ui/mediapicker/AudioRecordViewTest.java
new file mode 100644
index 0000000..a38dac2
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/mediapicker/AudioRecordViewTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.mediapicker;
+
+import android.media.MediaPlayer;
+import android.media.MediaRecorder.OnErrorListener;
+import android.media.MediaRecorder.OnInfoListener;
+import android.net.Uri;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.ui.ViewTest;
+import com.android.messaging.util.FakeMediaUtil;
+
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class AudioRecordViewTest extends ViewTest<AudioRecordView> {
+
+    @Mock AudioRecordView.HostInterface mockHost;
+    @Mock LevelTrackingMediaRecorder mockRecorder;
+    @Mock MediaPlayer mockMediaPlayer;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        FakeFactory.register(getInstrumentation().getContext())
+                .withMediaUtil(new FakeMediaUtil(mockMediaPlayer));
+    }
+
+    private void verifyAudioSubmitted() {
+        Mockito.verify(mockHost).onAudioRecorded(Matchers.any(MessagePartData.class));
+    }
+
+    private AudioRecordView initView() {
+        final AudioRecordView view = getView();
+        view.testSetMediaRecorder(mockRecorder);
+        view.setHostInterface(mockHost);
+        return view;
+    }
+
+    public void testRecording() {
+        Mockito.when(mockRecorder.isRecording()).thenReturn(false);
+        Mockito.when(mockRecorder.startRecording(Matchers.<OnErrorListener>any(),
+                Matchers.<OnInfoListener>any(), Matchers.anyInt())).thenReturn(true);
+        Mockito.when(mockRecorder.stopRecording()).thenReturn(Uri.parse("content://someaudio/2"));
+        final AudioRecordView view = initView();
+        view.onRecordButtonTouchDown();
+        Mockito.verify(mockRecorder).startRecording(Matchers.<OnErrorListener>any(),
+                Matchers.<OnInfoListener>any(), Matchers.anyInt());
+        Mockito.when(mockRecorder.isRecording()).thenReturn(true);
+        // Record for 1 second to make it meaningful.
+        sleepNoThrow(1000);
+        view.onRecordButtonTouchUp();
+        // We added some buffer to the end of the audio recording, so sleep for sometime and
+        // verify audio is recorded.
+        sleepNoThrow(700);
+        Mockito.verify(mockRecorder).stopRecording();
+        verifyAudioSubmitted();
+    }
+
+    private void sleepNoThrow(final long duration) {
+        try {
+            Thread.sleep(duration);
+        } catch (final InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        return R.layout.mediapicker_audio_chooser;
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/mediapicker/CameraManagerTest.java b/tests/src/com/android/messaging/ui/mediapicker/CameraManagerTest.java
new file mode 100644
index 0000000..951c694
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/mediapicker/CameraManagerTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.mediapicker;
+
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.os.AsyncTask;
+import android.test.suitebuilder.annotation.SmallTest;
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.ui.mediapicker.CameraManager.CameraWrapper;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+@SmallTest
+public class CameraManagerTest extends BugleTestCase {
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // Force each test to set up a camera wrapper to match their needs
+        CameraManager.setCameraWrapper(null);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        MockCameraFactory.cleanup();
+    }
+
+    public void testNoCameraDeviceGetInfo() {
+        CameraManager.setCameraWrapper(MockCameraFactory.createCameraWrapper());
+        assertEquals(false, CameraManager.get().hasAnyCamera());
+        assertEquals(false, CameraManager.get().hasFrontAndBackCamera());
+        try {
+            CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
+            fail("selectCamera should have thrown");
+        } catch (AssertionError e) {
+        }
+    }
+
+    public void testFrontFacingOnlyGetInfo() {
+        CameraManager.setCameraWrapper(MockCameraFactory.createCameraWrapper(
+                MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_FRONT)
+        ));
+        assertEquals(true, CameraManager.get().hasAnyCamera());
+        assertEquals(false, CameraManager.get().hasFrontAndBackCamera());
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_FRONT);
+        assertEquals(CameraInfo.CAMERA_FACING_FRONT, CameraManager.get().getCameraInfo().facing);
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
+        assertEquals(CameraInfo.CAMERA_FACING_FRONT, CameraManager.get().getCameraInfo().facing);
+    }
+
+    public void testBackFacingOnlyGetInfo() {
+        CameraManager.setCameraWrapper(MockCameraFactory.createCameraWrapper(
+                MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_BACK)
+        ));
+        assertEquals(true, CameraManager.get().hasAnyCamera());
+        assertEquals(false, CameraManager.get().hasFrontAndBackCamera());
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_FRONT);
+        assertEquals(CameraInfo.CAMERA_FACING_BACK, CameraManager.get().getCameraInfo().facing);
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
+        assertEquals(CameraInfo.CAMERA_FACING_BACK, CameraManager.get().getCameraInfo().facing);
+    }
+
+    public void testFrontAndBackGetInfo() {
+        CameraManager.setCameraWrapper(MockCameraFactory.createCameraWrapper(
+                MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_FRONT),
+                MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_BACK)
+        ));
+        assertEquals(true, CameraManager.get().hasAnyCamera());
+        assertEquals(true, CameraManager.get().hasFrontAndBackCamera());
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_FRONT);
+        assertEquals(CameraInfo.CAMERA_FACING_FRONT, CameraManager.get().getCameraInfo().facing);
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
+        assertEquals(CameraInfo.CAMERA_FACING_BACK, CameraManager.get().getCameraInfo().facing);
+    }
+
+    public void testSwapCamera() {
+        CameraManager.setCameraWrapper(MockCameraFactory.createCameraWrapper(
+                MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_FRONT),
+                MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_BACK)
+        ));
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_FRONT);
+        assertEquals(CameraInfo.CAMERA_FACING_FRONT, CameraManager.get().getCameraInfo().facing);
+        CameraManager.get().swapCamera();
+        assertEquals(CameraInfo.CAMERA_FACING_BACK, CameraManager.get().getCameraInfo().facing);
+    }
+
+    public void testOpenCamera() {
+        Camera backCamera = MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_BACK);
+        Camera frontCamera = MockCameraFactory.createCamera(CameraInfo.CAMERA_FACING_FRONT);
+        CameraWrapper wrapper = MockCameraFactory.createCameraWrapper(frontCamera, backCamera);
+        CameraManager.setCameraWrapper(wrapper);
+        CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
+        CameraManager.get().openCamera();
+        CameraManager.get().openCamera();
+        CameraManager.get().openCamera();
+        waitForPendingAsyncTasks();
+        Mockito.verify(wrapper, Mockito.never()).open(0);
+        Mockito.verify(wrapper).open(1);
+        Mockito.verify(wrapper, Mockito.never()).release(frontCamera);
+        Mockito.verify(wrapper, Mockito.never()).release(backCamera);
+        CameraManager.get().swapCamera();
+        waitForPendingAsyncTasks();
+        Mockito.verify(wrapper).open(0);
+        Mockito.verify(wrapper).open(1);
+        Mockito.verify(wrapper, Mockito.never()).release(frontCamera);
+        Mockito.verify(wrapper).release(backCamera);
+        InOrder inOrder = Mockito.inOrder(wrapper);
+        inOrder.verify(wrapper).open(1);
+        inOrder.verify(wrapper).release(backCamera);
+        inOrder.verify(wrapper).open(0);
+    }
+
+    private void waitForPendingAsyncTasks() {
+        try {
+            final Object lockObject = new Object();
+
+            new AsyncTask<Void, Void, Void>() {
+                @Override
+                protected Void doInBackground(Void... voids) {
+                    return null;
+                }
+
+                @Override
+                protected void onPostExecute(Void aVoid) {
+                    super.onPostExecute(aVoid);
+                    synchronized (lockObject) {
+                        lockObject.notifyAll();
+                    }
+                }
+            }.execute();
+
+            synchronized (lockObject) {
+                lockObject.wait(500);
+            }
+        } catch (InterruptedException e) {
+            fail();
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/mediapicker/GalleryGridItemViewTest.java b/tests/src/com/android/messaging/ui/mediapicker/GalleryGridItemViewTest.java
new file mode 100644
index 0000000..83d8ac9
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/mediapicker/GalleryGridItemViewTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.mediapicker;
+
+import android.content.Context;
+import android.provider.MediaStore.Images.Media;
+import android.view.View;
+import android.widget.CheckBox;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.FakeCursor;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.data.GalleryGridItemData;
+import com.android.messaging.datamodel.data.TestDataFactory;
+import com.android.messaging.ui.AsyncImageView;
+import com.android.messaging.ui.ViewTest;
+import com.android.messaging.util.UriUtil;
+
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class GalleryGridItemViewTest extends ViewTest<GalleryGridItemView> {
+
+    @Mock GalleryGridItemView.HostInterface mockHost;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final Context context = getInstrumentation().getTargetContext();
+        FakeFactory.register(context)
+            .withDataModel(new FakeDataModel(context));
+    }
+
+    protected void verifyClickedItem(final View view, final GalleryGridItemData data) {
+        Mockito.verify(mockHost).onItemClicked(view, data, false /* longClick */);
+    }
+
+    protected void verifyContent(
+            final GalleryGridItemView view,
+            final String imageUrl,
+            final boolean showCheckbox,
+            final boolean isSelected) {
+        final AsyncImageView imageView = (AsyncImageView) view.findViewById(R.id.image);
+        final CheckBox checkBox = (CheckBox) view.findViewById(R.id.checkbox);
+
+        assertNotNull(imageView);
+        assertTrue(imageView.mImageRequestBinding.isBound());
+        assertTrue(imageView.mImageRequestBinding.getData().getKey().startsWith(imageUrl));
+        assertNotNull(checkBox);
+        if (showCheckbox) {
+            assertEquals(View.VISIBLE, checkBox.getVisibility());
+            assertEquals(isSelected, checkBox.isChecked());
+        } else {
+            assertNotSame(View.VISIBLE, checkBox.getVisibility());
+        }
+    }
+
+    public void testBind() {
+        Mockito.when(mockHost.isMultiSelectEnabled()).thenReturn(false);
+        Mockito.when(mockHost.isItemSelected(Matchers.<GalleryGridItemData>any()))
+                .thenReturn(false);
+        final GalleryGridItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getGalleryGridCursor();
+        cursor.moveToFirst();
+        final String path = (String) cursor.getAt(Media.DATA, 0);
+        view.bind(cursor, mockHost);
+        verifyContent(view, UriUtil.getUriForResourceFile(path).toString(),
+                false, false);
+    }
+
+    public void testBindMultiSelectUnSelected() {
+        Mockito.when(mockHost.isMultiSelectEnabled()).thenReturn(true);
+        Mockito.when(mockHost.isItemSelected(Matchers.<GalleryGridItemData>any()))
+                .thenReturn(false);
+        final GalleryGridItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getGalleryGridCursor();
+        cursor.moveToFirst();
+        final String path = (String) cursor.getAt(Media.DATA, 0);
+        view.bind(cursor, mockHost);
+        verifyContent(view, UriUtil.getUriForResourceFile(path).toString(),
+                true, false);
+    }
+
+    public void testBindMultiSelectSelected() {
+        Mockito.when(mockHost.isMultiSelectEnabled()).thenReturn(true);
+        Mockito.when(mockHost.isItemSelected(Matchers.<GalleryGridItemData>any()))
+                .thenReturn(true);
+        final GalleryGridItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getGalleryGridCursor();
+        cursor.moveToFirst();
+        final String path = (String) cursor.getAt(Media.DATA, 0);
+        view.bind(cursor, mockHost);
+        verifyContent(view, UriUtil.getUriForResourceFile(path).toString(),
+                true, true);
+    }
+
+    public void testClick() {
+        Mockito.when(mockHost.isMultiSelectEnabled()).thenReturn(false);
+        Mockito.when(mockHost.isItemSelected(Matchers.<GalleryGridItemData>any()))
+                .thenReturn(false);
+        final GalleryGridItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getGalleryGridCursor();
+        cursor.moveToFirst();
+        view.bind(cursor, mockHost);
+        view.performClick();
+        verifyClickedItem(view, view.mData);
+    }
+
+    public void testBindTwice() {
+        Mockito.when(mockHost.isMultiSelectEnabled()).thenReturn(true);
+        Mockito.when(mockHost.isItemSelected(Matchers.<GalleryGridItemData>any()))
+                .thenReturn(false);
+        final GalleryGridItemView view = getView();
+        final FakeCursor cursor = TestDataFactory.getGalleryGridCursor();
+
+        cursor.moveToFirst();
+        view.bind(cursor, mockHost);
+
+        cursor.moveToNext();
+        final String path = (String) cursor.getAt(Media.DATA, 1);
+        view.bind(cursor, mockHost);
+        verifyContent(view, UriUtil.getUriForResourceFile(path).toString(),
+                true, false);
+    }
+
+    @Override
+    protected int getLayoutIdForView() {
+        return R.layout.gallery_grid_item_view;
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/mediapicker/MediaPickerTest.java b/tests/src/com/android/messaging/ui/mediapicker/MediaPickerTest.java
new file mode 100644
index 0000000..4a7040e
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/mediapicker/MediaPickerTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.mediapicker;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+
+import com.android.messaging.FakeFactory;
+import com.android.messaging.R;
+import com.android.messaging.datamodel.FakeDataModel;
+import com.android.messaging.datamodel.binding.Binding;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.datamodel.data.DraftMessageData;
+import com.android.messaging.datamodel.data.MediaPickerData;
+import com.android.messaging.ui.FragmentTestCase;
+
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class MediaPickerTest extends FragmentTestCase<MediaPicker> {
+    @Mock protected MediaPickerData mMockMediaPickerData;
+    @Mock protected DraftMessageData mMockDraftMessageData;
+    protected FakeDataModel mFakeDataModel;
+
+    public MediaPickerTest() {
+        super(MediaPicker.class);
+    }
+
+    @Override
+    protected MediaPicker getFragment() {
+        if (mFragment == null) {
+            mFragment = new MediaPicker(getInstrumentation().getTargetContext());
+        }
+        return mFragment;
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final Context context = getInstrumentation().getTargetContext();
+        mFakeDataModel = new FakeDataModel(context)
+                   .withMediaPickerData(mMockMediaPickerData);
+        FakeFactory.register(context)
+                   .withDataModel(mFakeDataModel);
+    }
+
+    /**
+     * Helper method to initialize the MediaPicker and its data.
+     */
+    private void initFragment(final int supportedMediaTypes, final Integer[] expectedLoaderIds,
+            final boolean filterTabBeforeAttach) {
+        Mockito.when(mMockMediaPickerData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+        Mockito.when(mMockDraftMessageData.isBound(Matchers.anyString()))
+            .thenReturn(true);
+        final Binding<DraftMessageData> draftBinding = BindingBase.createBinding(this);
+        draftBinding.bind(mMockDraftMessageData);
+
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final MediaPicker fragment = getFragment();
+                if (filterTabBeforeAttach) {
+                    fragment.setSupportedMediaTypes(supportedMediaTypes);
+                    getActivity().setFragment(fragment);
+                } else {
+                    getActivity().setFragment(fragment);
+                    fragment.setSupportedMediaTypes(supportedMediaTypes);
+                }
+                fragment.setDraftMessageDataModel(draftBinding);
+                Mockito.verify(mMockMediaPickerData,
+                        Mockito.atLeastOnce()).init(
+                        Matchers.eq(fragment.getLoaderManager()));
+                fragment.open(MediaPicker.MEDIA_TYPE_ALL, false);
+            }
+        });
+        getInstrumentation().waitForIdleSync();
+    }
+
+    public void testDefaultTabs() {
+        Mockito.when(mMockMediaPickerData.getSelectedChooserIndex()).thenReturn(0);
+        initFragment(MediaPicker.MEDIA_TYPE_ALL, new Integer[] {
+                MediaPickerData.GALLERY_IMAGE_LOADER },
+                false);
+        final MediaPicker mediaPicker = getFragment();
+        final View view = mediaPicker.getView();
+        assertNotNull(view);
+        final ViewGroup tabStrip = (ViewGroup) view.findViewById(R.id.mediapicker_tabstrip);
+        assertEquals(tabStrip.getChildCount(), 3);
+        for (int i = 0; i < tabStrip.getChildCount(); i++) {
+            final ImageButton tabButton = (ImageButton) tabStrip.getChildAt(i);
+            assertEquals(View.VISIBLE, tabButton.getVisibility());
+            assertEquals(i == 0, tabButton.isSelected());
+        }
+    }
+
+    public void testFilterTabsBeforeAttach() {
+        Mockito.when(mMockMediaPickerData.getSelectedChooserIndex()).thenReturn(0);
+        initFragment(MediaPicker.MEDIA_TYPE_IMAGE, new Integer[] {
+                MediaPickerData.GALLERY_IMAGE_LOADER },
+                true);
+        final MediaPicker mediaPicker = getFragment();
+        final View view = mediaPicker.getView();
+        assertNotNull(view);
+        final ViewGroup tabStrip = (ViewGroup) view.findViewById(R.id.mediapicker_tabstrip);
+        assertEquals(tabStrip.getChildCount(), 3);
+        for (int i = 0; i < tabStrip.getChildCount(); i++) {
+            final ImageButton tabButton = (ImageButton) tabStrip.getChildAt(i);
+            assertEquals(i == 0, tabButton.isSelected());
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/ui/mediapicker/MockCameraFactory.java b/tests/src/com/android/messaging/ui/mediapicker/MockCameraFactory.java
new file mode 100644
index 0000000..789a78f
--- /dev/null
+++ b/tests/src/com/android/messaging/ui/mediapicker/MockCameraFactory.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2015 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.messaging.ui.mediapicker;
+
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+
+import com.android.messaging.ui.mediapicker.CameraManager.CameraWrapper;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class MockCameraFactory {
+    private static Map<Camera, CameraInfo> sCameraInfos = new HashMap<Camera, CameraInfo>();
+
+    public static Camera createCamera(int facing) {
+        Camera camera = Mockito.mock(Camera.class);
+        CameraInfo cameraInfo = new CameraInfo();
+        cameraInfo.facing = facing;
+        sCameraInfos.put(camera, cameraInfo);
+        return camera;
+    }
+
+    public static void getCameraInfo(Camera camera, CameraInfo outCameraInfo) {
+        CameraInfo cameraInfo = sCameraInfos.get(camera);
+        outCameraInfo.facing = cameraInfo.facing;
+        outCameraInfo.orientation = cameraInfo.orientation;
+        outCameraInfo.canDisableShutterSound = cameraInfo.canDisableShutterSound;
+    }
+
+    public static CameraWrapper createCameraWrapper(final Camera... cameras) {
+        CameraWrapper wrapper = Mockito.mock(CameraWrapper.class);
+        Mockito.when(wrapper.getNumberOfCameras()).thenReturn(cameras.length);
+        Mockito.when(wrapper.open(Mockito.anyInt())).then(new Answer<Camera>() {
+            @Override
+            public Camera answer(InvocationOnMock invocation) {
+                return cameras[(Integer) invocation.getArguments()[0]];
+            }
+        });
+        Mockito.doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getCameraInfo(
+                        cameras[(Integer) invocation.getArguments()[0]],
+                        (CameraInfo) invocation.getArguments()[1]
+                );
+                return null;
+            }
+        }).when(wrapper).getCameraInfo(Mockito.anyInt(), Mockito.any(CameraInfo.class));
+        return wrapper;
+    }
+
+    public static void cleanup() {
+        sCameraInfos.clear();
+    }
+}
diff --git a/tests/src/com/android/messaging/util/BugleGservicesTest.java b/tests/src/com/android/messaging/util/BugleGservicesTest.java
new file mode 100644
index 0000000..1a0a10e
--- /dev/null
+++ b/tests/src/com/android/messaging/util/BugleGservicesTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.messaging.util;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+
+@SmallTest
+public class BugleGservicesTest extends BugleTestCase {
+
+    public void testGServiceGet() {
+        final BugleGservices bugleGservices = new FakeBugleGservices();
+
+        assertEquals(BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX_DEFAULT,
+                bugleGservices.getString(
+                        BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX,
+                        BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX_DEFAULT));
+    }
+}
diff --git a/tests/src/com/android/messaging/util/ContactUtilTest.java b/tests/src/com/android/messaging/util/ContactUtilTest.java
new file mode 100644
index 0000000..48a7ced
--- /dev/null
+++ b/tests/src/com/android/messaging/util/ContactUtilTest.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2015 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.messaging.util;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.text.TextUtils;
+
+import com.android.messaging.BugleTestCase;
+import com.android.messaging.FakeFactory;
+
+import org.junit.Assert;
+
+import java.util.ArrayList;
+
+/*
+ * Class for testing ContactUtil.
+ */
+@LargeTest
+public class ContactUtilTest extends BugleTestCase {
+    private static final String TEST_NAME_PREFIX = "BugleTest:";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // TODO: This test will actually mess with contacts on your phone.
+        // Ideally we would use a fake content provider to give us contact data...
+        FakeFactory.registerWithoutFakeContext(getTestContext());
+
+        // add test contacts.
+        addTestContact("John", "650-123-1233", "john@gmail.com", false);
+        addTestContact("Joe", "(650)123-1233", "joe@gmail.com", false);
+        addTestContact("Jim", "650 123 1233", "jim@gmail.com", false);
+        addTestContact("Samantha", "650-123-1235", "samantha@gmail.com", true);
+        addTestContact("Adrienne", "650-123-1236", "adrienne@gmail.com", true);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        deleteTestContacts();
+        super.tearDown();
+    }
+
+    /**
+     * Add a test contact based on contact name, phone and email.
+     */
+    private void addTestContact(
+            final String name, final String phone, final String email, final boolean starred)
+            throws Exception {
+        final ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+        ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+                .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
+                .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
+                .build());
+
+        ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+                .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+                .withValue(ContactsContract.Data.MIMETYPE,
+                        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+                .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
+                        TEST_NAME_PREFIX + name).build());
+
+        if (phone != null) {
+            ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+                    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+                    .withValue(ContactsContract.Data.MIMETYPE,
+                            ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+                    .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
+                    .withValue(ContactsContract.CommonDataKinds.Phone.TYPE,
+                            ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
+                    .build());
+        }
+
+        if (email != null) {
+            ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+                    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+                    .withValue(ContactsContract.Data.MIMETYPE,
+                            ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+                    .withValue(ContactsContract.CommonDataKinds.Email.DATA, email)
+                    .withValue(ContactsContract.CommonDataKinds.Email.TYPE,
+                            ContactsContract.CommonDataKinds.Email.TYPE_WORK)
+                    .build());
+        }
+
+        mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
+
+        // Star the whole contact if needed.
+        if (starred) {
+            final ContentValues values = new ContentValues();
+            values.put(Contacts.STARRED, 1);
+            getContext().getContentResolver().update(Contacts.CONTENT_URI, values,
+                    Contacts.DISPLAY_NAME + "= ?", new String[] { TEST_NAME_PREFIX + name });
+        }
+    }
+
+    /**
+     * Remove test contacts added during test setup.
+     */
+    private void deleteTestContacts() {
+        final Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
+                Uri.encode(TEST_NAME_PREFIX));
+        final Cursor cur =
+                mContext.getContentResolver().query(contactUri, null, null, null, null);
+        try {
+            if (cur.moveToFirst()) {
+                do {
+                    final String lookupKey = cur.getString(cur.getColumnIndex(Contacts.LOOKUP_KEY));
+                    final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+                    mContext.getContentResolver().delete(uri, null, null);
+                } while (cur.moveToNext());
+            }
+        } catch (final Exception e) {
+            System.out.println(e.getStackTrace());
+        }
+    }
+
+    /**
+     * Verify ContactUtil.getPhone will return all phones, including the ones added for test.
+     */
+    public void ingoredTestGetPhones() {
+        final Cursor cur = ContactUtil.getPhones(getContext())
+                .performSynchronousQuery();
+
+        LogUtil.i(LogUtil.BUGLE_TAG, "testGetPhones: Number of phones on the device:" +
+                cur.getCount());
+
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Joe");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Jim");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Samantha");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Adrienne");
+    }
+
+    /**
+     * Verify ContactUtil.filterPhone will work on name based matches.
+     */
+    public void ingoredTestFilterPhonesByName() {
+        final Cursor cur = ContactUtil.filterPhones(getContext(), TEST_NAME_PREFIX)
+                .performSynchronousQuery();
+
+        if (cur.getCount() != 5) {
+            Assert.fail("Cursor should have size of 5");
+            return;
+        }
+
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Joe");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Jim");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Samantha");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Adrienne");
+    }
+
+    /**
+     * Verify ContactUtil.filterPhone will work on partial number matches.
+     */
+    public void ingoredTestFilterPhonesByPartialNumber() {
+        final String[] filters = new String[] { "650123", "650-123", "(650)123", "650 123" };
+
+        for (final String filter : filters) {
+            final Cursor cur = ContactUtil.filterPhones(getContext(), filter)
+                    .performSynchronousQuery();
+
+            LogUtil.i(LogUtil.BUGLE_TAG, "testFilterPhonesByPartialNumber: Number of phones:" +
+                    cur.getCount());
+
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Joe");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Jim");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Samantha");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Adrienne");
+        }
+    }
+
+    /**
+     * Verify ContactUtil.filterPhone will work on full number matches.
+     */
+    public void ingoredTestFilterPhonesByFullNumber() {
+        final String[] filters = new String[] {
+                "6501231233", "650-123-1233", "(650)123-1233", "650 123 1233" };
+
+        for (final String filter : filters) {
+            final Cursor cur = ContactUtil.filterPhones(getContext(), filter)
+                    .performSynchronousQuery();
+
+            LogUtil.i(LogUtil.BUGLE_TAG, "testFilterPhonesByFullNumber: Number of phones:" +
+                    cur.getCount());
+
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Joe");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Jim");
+        }
+    }
+
+    /**
+     * Verify ContactUtil.lookPhone will work on number including area code.
+     */
+    public void ingoredTestLookupPhoneWithAreaCode() {
+        final String[] filters = new String[] {
+                "6501231233", "650-123-1233", "(650)123-1233", "650 123 1233" };
+
+        for (final String filter : filters) {
+            final Cursor cur = ContactUtil.lookupPhone(getContext(), filter)
+                    .performSynchronousQuery();
+
+            LogUtil.i(LogUtil.BUGLE_TAG, "testLookupPhoneWithAreaCode: Number of phones:" +
+                    cur.getCount());
+
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Joe");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Jim");
+        }
+    }
+
+    /**
+     * Verify ContactUtil.lookPhone will work on number without area code.
+     */
+    public void ingoredTestLookupPhoneWithoutAreaCode() {
+        final String[] filters = new String[] {
+                "1231233", "123-1233", "123 1233" };
+
+        for (final String filter : filters) {
+            final Cursor cur = ContactUtil.lookupPhone(getContext(), filter)
+                    .performSynchronousQuery();
+
+            LogUtil.i(LogUtil.BUGLE_TAG, "testLookupPhoneWithoutAreaCode: Number of phones:" +
+                    cur.getCount());
+
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Joe");
+            verifyCursorContains(cur, TEST_NAME_PREFIX + "Jim");
+        }
+    }
+
+    public void ingoredTestGetFrequentPhones() {
+        final Cursor cur = ContactUtil.getFrequentContacts(getContext())
+                .performSynchronousQuery();
+
+        LogUtil.i(LogUtil.BUGLE_TAG, "testGetFrequentPhones: Number of phones on the device:" +
+                cur.getCount());
+
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Samantha");
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "Adrienne");
+    }
+
+    /**
+     * Verify ContactUtil.filterEmails will work on partial email.
+     */
+    public void ingoredTestFilterEmails() {
+        final Cursor cur = ContactUtil.filterEmails(getContext(), "john@")
+                .performSynchronousQuery();
+
+        LogUtil.i(LogUtil.BUGLE_TAG, "testFilterEmails: Number of emails:" +
+                cur.getCount());
+
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+    }
+
+    /**
+     * Verify ContactUtil.lookupEmail will work on full email.
+     */
+    public void ingoredTestLookupEmail() {
+        final Cursor cur = ContactUtil.lookupEmail(getContext(), "john@gmail.com")
+                .performSynchronousQuery();
+
+        LogUtil.i(LogUtil.BUGLE_TAG, "testLookupEmail: Number of emails:" +
+                cur.getCount());
+
+        verifyCursorContains(cur, TEST_NAME_PREFIX + "John");
+    }
+
+    /**
+     * Utility method to check whether cursor contains a particular contact.
+     */
+    private void verifyCursorContains(final Cursor cursor, final String nameToVerify) {
+        if (cursor.moveToFirst()) {
+            do {
+                final String name = cursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
+                if (TextUtils.equals(name, nameToVerify)) {
+                    return;
+                }
+            } while (cursor.moveToNext());
+        }
+        Assert.fail("Cursor should have " + nameToVerify);
+    }
+}
diff --git a/tests/src/com/android/messaging/util/FakeBugleGservices.java b/tests/src/com/android/messaging/util/FakeBugleGservices.java
new file mode 100644
index 0000000..f7d90da
--- /dev/null
+++ b/tests/src/com/android/messaging/util/FakeBugleGservices.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 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.messaging.util;
+
+/**
+ * Fake implementation which just returns the default values.
+ */
+public class FakeBugleGservices extends BugleGservices {
+    public FakeBugleGservices() {
+    }
+
+    @Override
+    public void registerForChanges(final Runnable r) {
+    }
+
+    @Override
+    public long getLong(final String key, final long defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public int getInt(final String key, final int defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public boolean getBoolean(final String key, final boolean defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public String getString(final String key, final String defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public float getFloat(String key, float defaultValue) {
+        return defaultValue;
+    }
+
+}
diff --git a/tests/src/com/android/messaging/util/FakeBuglePrefs.java b/tests/src/com/android/messaging/util/FakeBuglePrefs.java
new file mode 100644
index 0000000..b3429a6
--- /dev/null
+++ b/tests/src/com/android/messaging/util/FakeBuglePrefs.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2015 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.messaging.util;
+
+
+/**
+ * Fake implementation which just returns the default values and ignores put operations.
+ */
+public class FakeBuglePrefs extends BuglePrefs {
+    public FakeBuglePrefs() {
+    }
+
+    @Override
+    public int getInt(final String key, final int defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public long getLong(final String key, final long defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public boolean getBoolean(final String key, final boolean defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public String getString(final String key, final String defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public byte[] getBytes(String key) {
+        return null;
+    }
+
+    @Override
+    public void putInt(final String key, final int value) {
+    }
+
+    @Override
+    public void putLong(final String key, final long value) {
+    }
+
+    @Override
+    public void putBoolean(final String key, final boolean value) {
+    }
+
+    @Override
+    public void putString(final String key, final String value) {
+    }
+
+    @Override
+    public void putBytes(String key, byte[] value) {
+    }
+
+    @Override
+    public String getSharedPreferencesName() {
+        return "FakeBuglePrefs";
+    }
+
+    @Override
+    public void onUpgrade(int oldVersion, int newVersion) {
+    }
+
+    @Override
+    public void remove(String key) {
+    }
+}
diff --git a/tests/src/com/android/messaging/util/FakeMediaUtil.java b/tests/src/com/android/messaging/util/FakeMediaUtil.java
new file mode 100644
index 0000000..e37ff0c
--- /dev/null
+++ b/tests/src/com/android/messaging/util/FakeMediaUtil.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.messaging.util;
+
+import android.content.Context;
+import android.media.MediaPlayer;
+
+public class FakeMediaUtil extends MediaUtil {
+    private final MediaPlayer mMediaPlayer;
+    public FakeMediaUtil(final MediaPlayer mediaPlayer) {
+        mMediaPlayer = mediaPlayer;
+    }
+
+    @Override
+    public void playSound(Context context, int resId, OnCompletionListener completionListener) {
+        if (completionListener != null) {
+            completionListener.onCompletion();
+        }
+    }
+}
diff --git a/tests/src/com/android/messaging/util/YouTubeUtilTest.java b/tests/src/com/android/messaging/util/YouTubeUtilTest.java
new file mode 100644
index 0000000..03c461a
--- /dev/null
+++ b/tests/src/com/android/messaging/util/YouTubeUtilTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 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.messaging.util;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.messaging.BugleTestCase;
+
+/*
+ * Class for testing YouTubeUtil.
+ */
+@SmallTest
+public class YouTubeUtilTest extends BugleTestCase {
+    public void testGetYoutubePreviewImageLink() {
+        final String videoId = "dQw4w9WgXcQ";
+        final String videoThumbnailUrl = YouTubeUtil.YOUTUBE_STATIC_THUMBNAIL_PREFIX + videoId
+                + YouTubeUtil.YOUTUBE_STATIC_THUMBNAIL_END;
+
+        // Check known valid youtube links to videos
+        assertEquals(
+                YouTubeUtil.getYoutubePreviewImageLink("http://www.youtube.com/watch?v=" + videoId),
+                videoThumbnailUrl);
+        assertEquals(
+                YouTubeUtil.getYoutubePreviewImageLink("https://www.youtube.com/watch?v=" + videoId
+                        + "&feature=youtu.be"), videoThumbnailUrl);
+        assertEquals(
+                YouTubeUtil.getYoutubePreviewImageLink("www.youtube.com/watch?v=" + videoId),
+                videoThumbnailUrl);
+        assertEquals(
+                YouTubeUtil.getYoutubePreviewImageLink("http://www.youtube.com/embed/" + videoId),
+                videoThumbnailUrl);
+        assertEquals(YouTubeUtil.getYoutubePreviewImageLink("http://www.youtube.com/v/" + videoId),
+                videoThumbnailUrl);
+        assertEquals(
+                YouTubeUtil.getYoutubePreviewImageLink("https://youtube.googleapis.com/v/"
+                        + videoId), videoThumbnailUrl);
+        assertEquals(
+                YouTubeUtil.getYoutubePreviewImageLink("http://www.youtube.com/apiplayer?video_id="
+                        + videoId), videoThumbnailUrl);
+        // This is the type of links that are used as shares from YouTube and will be the most
+        // likely case that we see
+        assertEquals(YouTubeUtil.getYoutubePreviewImageLink("http://youtu.be/" + videoId),
+                videoThumbnailUrl);
+
+        // Try links that shouldn't work
+        assertNull(YouTubeUtil.getYoutubePreviewImageLink("http://www.youtube.com"));
+    }
+}
\ No newline at end of file