Merge "Map Client use content provider for messages"
diff --git a/src/com/android/bluetooth/map/BluetoothMapbMessageMime.java b/src/com/android/bluetooth/map/BluetoothMapbMessageMime.java
index 2dbdd1d..1d466c5 100644
--- a/src/com/android/bluetooth/map/BluetoothMapbMessageMime.java
+++ b/src/com/android/bluetooth/map/BluetoothMapbMessageMime.java
@@ -46,7 +46,7 @@
                                             * jpeg data or the text.getBytes("utf-8") */
 
 
-        String getDataAsString() {
+        public String getDataAsString() {
             String result = null;
             String charset = mCharsetName;
             // Figure out if we support the charset, else fall back to UTF-8, as this is what
diff --git a/src/com/android/bluetooth/mapclient/MapClientContent.java b/src/com/android/bluetooth/mapclient/MapClientContent.java
new file mode 100644
index 0000000..57d201f
--- /dev/null
+++ b/src/com/android/bluetooth/mapclient/MapClientContent.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2020 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.bluetooth.mapclient;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Telephony;
+import android.provider.Telephony.Mms;
+import android.provider.Telephony.MmsSms;
+import android.provider.Telephony.Sms;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.bluetooth.map.BluetoothMapbMessageMime;
+import com.android.bluetooth.map.BluetoothMapbMessageMime.MimePart;
+import com.android.vcard.VCardConstants;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardProperty;
+
+import com.google.android.mms.pdu.PduHeaders;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+
+class MapClientContent {
+
+    private static final String INBOX_PATH = "telecom/msg/inbox";
+    private static final String TAG = "MapClientContent";
+    private static final int DEFAULT_CHARSET = 106;
+    private static final int ORIGINATOR_ADDRESS_TYPE = 137;
+    private static final int RECIPIENT_ADDRESS_TYPE = 151;
+
+    final BluetoothDevice mDevice;
+    private final Context mContext;
+    private final Callbacks mCallbacks;
+    private final ContentResolver mResolver;
+    ContentObserver mContentObserver;
+    String mPhoneNumber = null;
+    private int mSubscriptionId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+    private SubscriptionManager mSubscriptionManager;
+    private HashMap<String, Uri> mHandleToUriMap = new HashMap<>();
+    private HashMap<Uri, MessageStatus> mUriToHandleMap = new HashMap<>();
+
+    /**
+     * Callbacks
+     * API to notify about statusChanges as observed from the content provider
+     */
+    interface Callbacks {
+        void onMessageStatusChanged(String handle, int status);
+    }
+
+    /**
+     * MapClientContent manages all interactions between Bluetooth and the messaging provider.
+     *
+     * Changes to the database are mirrored between the remote and local providers, specifically new
+     * messages, changes to read status, and removal of messages.
+     *
+     * context: the context that all content provider interactions are conducted
+     * MceStateMachine:  the interface to send outbound updates such as when a message is read
+     * locally
+     * device: the associated Bluetooth device used for associating messages with a subscription
+     */
+    MapClientContent(Context context, Callbacks callbacks,
+            BluetoothDevice device) {
+        mContext = context;
+        mDevice = device;
+        mCallbacks = callbacks;
+        mResolver = mContext.getContentResolver();
+
+        mSubscriptionManager = (SubscriptionManager) mContext
+                .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        mSubscriptionManager
+                .addSubscriptionInfoRecord(device.getAddress(), /*device.getName()*/"TEST", 0,
+                        SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM);
+        SubscriptionInfo info = mSubscriptionManager
+                .getActiveSubscriptionInfoForIcc(mDevice.getAddress());
+        if (info != null) {
+            mSubscriptionId = info.getSubscriptionId();
+            mSubscriptionManager.setDisplayNumber(mPhoneNumber, mSubscriptionId);
+        }
+
+        mContentObserver = new ContentObserver(null) {
+            @Override
+            public boolean deliverSelfNotifications() {
+                return false;
+            }
+
+            @Override
+            public void onChange(boolean selfChange) {
+                logV("onChange");
+                findChangeInDatabase();
+            }
+
+            @Override
+            public void onChange(boolean selfChange, Uri uri) {
+                logV("onChange" + uri.toString());
+                findChangeInDatabase();
+            }
+        };
+
+        clearMessages();
+        mResolver.registerContentObserver(Sms.CONTENT_URI, true, mContentObserver);
+        mResolver.registerContentObserver(Mms.CONTENT_URI, true, mContentObserver);
+        mResolver.registerContentObserver(MmsSms.CONTENT_URI, true, mContentObserver);
+    }
+
+    private static void logD(String message) {
+        if (MapClientService.DBG) {
+            Log.d(TAG, message);
+        }
+    }
+
+    private static void logV(String message) {
+        if (MapClientService.VDBG) {
+            Log.v(TAG, message);
+        }
+    }
+
+    /**
+     * parseLocalNumber
+     *
+     * Determine the connected phone's number by extracting it from an inbound or outbound mms
+     * message.  This number is necessary such that group messages can be displayed correctly.
+     */
+    void parseLocalNumber(Bmessage message) {
+        if (mPhoneNumber != null) {
+            return;
+        }
+        if (INBOX_PATH.equals(message.getFolder())) {
+            ArrayList<VCardEntry> recipients = message.getRecipients();
+            if (recipients != null && !recipients.isEmpty()) {
+                mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(
+                        recipients.get(0).getPhoneList().get(0).getNumber());
+            }
+        } else {
+            mPhoneNumber = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
+        }
+        logV("Found phone number: " + mPhoneNumber);
+    }
+
+    /**
+     * storeMessage
+     *
+     * Store a message in database with the associated handle and timestamp.
+     * The handle is used to associate the local message with the remote message.
+     */
+    void storeMessage(Bmessage message, String handle, Long timestamp) {
+        switch (message.getType()) {
+            case MMS:
+                storeMms(message, handle, timestamp);
+                return;
+            case SMS_CDMA:
+            case SMS_GSM:
+                storeSms(message, handle, timestamp);
+                return;
+            default:
+                logD("Request to store unsupported message type: " + message.getType());
+        }
+    }
+
+    private void storeSms(Bmessage message, String handle, Long timestamp) {
+        logD("storeSms");
+        logV(message.toString());
+        VCardEntry originator = message.getOriginator();
+        String recipients;
+        if (INBOX_PATH.equals(message.getFolder())) {
+            recipients = getOriginatorNumber(message);
+        } else {
+            recipients = message.getRecipients().get(0).getPhoneList().get(0).getNumber();
+        }
+        logV("Received SMS from Number " + recipients);
+        String messageContent;
+
+        Uri contentUri = INBOX_PATH.equalsIgnoreCase(message.getFolder()) ? Sms.Inbox.CONTENT_URI
+                : Sms.Sent.CONTENT_URI;
+        ContentValues values = new ContentValues();
+        long threadId = getThreadId(message);
+        int readStatus = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
+
+        values.put(Sms.THREAD_ID, threadId);
+        values.put(Sms.ADDRESS, recipients);
+        values.put(Sms.BODY, message.getBodyContent());
+        values.put(Sms.SUBSCRIPTION_ID, mSubscriptionId);
+        values.put(Sms.DATE, timestamp);
+        values.put(Sms.READ, readStatus);
+
+        Uri results = mResolver.insert(contentUri, values);
+        mHandleToUriMap.put(handle, results);
+        mUriToHandleMap.put(results, new MessageStatus(handle, readStatus));
+        logD("Map InsertedThread" + results);
+    }
+
+    /**
+     * deleteMessage
+     * remove a message from the local provider based on a remote change
+     */
+    void deleteMessage(String handle) {
+        logD("deleting handle" + handle);
+        Uri messageToChange = mHandleToUriMap.get(handle);
+        if (messageToChange != null) {
+            mResolver.delete(messageToChange, null);
+        }
+    }
+
+
+    /**
+     * markRead
+     * mark a message read in the local provider based on a remote change
+     */
+    void markRead(String handle) {
+        logD("marking read " + handle);
+        Uri messageToChange = mHandleToUriMap.get(handle);
+        if (messageToChange != null) {
+            ContentValues values = new ContentValues();
+            values.put(Sms.READ, 1);
+            mResolver.update(messageToChange, values, null);
+        }
+    }
+
+    /**
+     * findChangeInDatabase
+     * compare the current state of the local content provider to the expected state and propagate
+     * changes to the remote.
+     */
+    private void findChangeInDatabase() {
+        HashMap<Uri, MessageStatus> originalUriToHandleMap;
+        HashMap<Uri, MessageStatus> duplicateUriToHandleMap;
+
+        originalUriToHandleMap = mUriToHandleMap;
+        duplicateUriToHandleMap = new HashMap<>(originalUriToHandleMap);
+        for (Uri uri : new Uri[]{Mms.CONTENT_URI, Sms.CONTENT_URI}) {
+            Cursor cursor = mResolver.query(uri, null, null, null, null);
+            while (cursor.moveToNext()) {
+                Uri index = Uri
+                        .withAppendedPath(uri, cursor.getString(cursor.getColumnIndex("_id")));
+                int readStatus = cursor.getInt(cursor.getColumnIndex(Sms.READ));
+                MessageStatus currentMessage = duplicateUriToHandleMap.remove(index);
+                if (currentMessage != null && currentMessage.mRead != readStatus) {
+                    logV(currentMessage.mHandle);
+                    currentMessage.mRead = readStatus;
+                    mCallbacks.onMessageStatusChanged(currentMessage.mHandle,
+                            BluetoothMapClient.READ);
+                }
+            }
+        }
+        for (HashMap.Entry record : duplicateUriToHandleMap.entrySet()) {
+            logV("Deleted " + ((MessageStatus) record.getValue()).mHandle);
+            originalUriToHandleMap.remove(record.getKey());
+            mCallbacks.onMessageStatusChanged(((MessageStatus) record.getValue()).mHandle,
+                    BluetoothMapClient.DELETED);
+        }
+    }
+
+    private void storeMms(Bmessage message, String handle, Long timestamp) {
+        logD("storeMms");
+        logV(message.toString());
+        try {
+            parseLocalNumber(message);
+            ContentValues values = new ContentValues();
+            long threadId = getThreadId(message);
+            BluetoothMapbMessageMime mmsBmessage = new BluetoothMapbMessageMime();
+            mmsBmessage.parseMsgPart(message.getBodyContent());
+            int read = message.getStatus() == Bmessage.Status.READ ? 1 : 0;
+            logD("Parsed");
+            values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId);
+            values.put(Mms.THREAD_ID, threadId);
+            values.put(Mms.DATE, timestamp / 1000L);
+            values.put(Mms.TEXT_ONLY, true);
+            values.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_INBOX);
+            values.put(Mms.READ, read);
+            values.put(Mms.SEEN, 0);
+            values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
+            values.put(Mms.MMS_VERSION, PduHeaders.CURRENT_MMS_VERSION);
+            values.put(Mms.PRIORITY, PduHeaders.PRIORITY_NORMAL);
+            values.put(Mms.READ_REPORT, PduHeaders.VALUE_NO);
+            values.put(Mms.TRANSACTION_ID, "T" + Long.toHexString(System.currentTimeMillis()));
+            values.put(Mms.DELIVERY_REPORT, PduHeaders.VALUE_NO);
+            values.put(Mms.LOCKED, 0);
+            values.put(Mms.CONTENT_TYPE, "application/vnd.wap.multipart.related");
+            values.put(Mms.MESSAGE_CLASS, PduHeaders.MESSAGE_CLASS_PERSONAL_STR);
+            values.put(Mms.MESSAGE_SIZE, mmsBmessage.getSize());
+
+            Uri results = mResolver.insert(Mms.CONTENT_URI, values);
+
+            logD("Map InsertedThread" + results);
+
+            for (MimePart part : mmsBmessage.getMimeParts()) {
+                storeMmsPart(part, results);
+            }
+
+            storeAddressPart(message, results);
+
+            Uri contentUri =
+                    INBOX_PATH.equalsIgnoreCase(message.getFolder()) ? Mms.Inbox.CONTENT_URI
+                            : Mms.Sent.CONTENT_URI;
+
+            String messageContent = mmsBmessage.getMessageAsText();
+
+            values.put(Mms.Part.CONTENT_TYPE, "plain/text");
+            values.put(Mms.SUBSCRIPTION_ID, mSubscriptionId);
+            mUriToHandleMap.put(results, new MessageStatus(handle, read));
+        } catch (Exception e) {
+            Log.e(TAG, e.toString());
+            throw e;
+        }
+    }
+
+    private Uri storeMmsPart(MimePart messagePart, Uri messageUri) {
+        ContentValues values = new ContentValues();
+        values.put(Mms.Part.CONTENT_TYPE, "text/plain");
+        values.put(Mms.Part.CHARSET, DEFAULT_CHARSET);
+        values.put(Mms.Part.FILENAME, "text_1.txt");
+        values.put(Mms.Part.NAME, "text_1.txt");
+        values.put(Mms.Part.CONTENT_ID, messagePart.mContentId);
+        values.put(Mms.Part.CONTENT_LOCATION, messagePart.mContentLocation);
+        values.put(Mms.Part.TEXT, messagePart.getDataAsString());
+
+        Uri contentUri = Uri.parse(messageUri.toString() + "/part");
+        Uri results = mResolver.insert(contentUri, values);
+        logD("Inserted" + results);
+        return results;
+    }
+
+    private void storeAddressPart(Bmessage message, Uri messageUri) {
+        ContentValues values = new ContentValues();
+        Uri contentUri = Uri.parse(messageUri.toString() + "/addr");
+        String originator = getOriginatorNumber(message);
+        values.put(Mms.Addr.CHARSET, DEFAULT_CHARSET);
+
+        values.put(Mms.Addr.ADDRESS, originator);
+        values.put(Mms.Addr.TYPE, ORIGINATOR_ADDRESS_TYPE);
+        mResolver.insert(contentUri, values);
+
+        Set<String> messageContacts = new ArraySet<>();
+        getRecipientsFromMessage(message, messageContacts);
+        for (String recipient : messageContacts) {
+            values.put(Mms.Addr.ADDRESS, recipient);
+            values.put(Mms.Addr.TYPE, RECIPIENT_ADDRESS_TYPE);
+            mResolver.insert(contentUri, values);
+        }
+    }
+
+    private Uri insertIntoMmsTable(String subject) {
+        ContentValues mmsValues = new ContentValues();
+        mmsValues.put(Mms.TEXT_ONLY, 1);
+        mmsValues.put(Mms.MESSAGE_TYPE, 128);
+        mmsValues.put(Mms.SUBJECT, subject);
+        return mResolver.insert(Mms.CONTENT_URI, mmsValues);
+    }
+
+    /**
+     * clearMessages
+     * clean up the content provider on startup and shutdown
+     */
+    void clearMessages() {
+        mResolver.unregisterContentObserver(mContentObserver);
+        mResolver.delete(Sms.CONTENT_URI, Sms.SUBSCRIPTION_ID + " =? ",
+                new String[]{Integer.toString(mSubscriptionId)});
+        mResolver.delete(Mms.CONTENT_URI, Mms.SUBSCRIPTION_ID + " =? ",
+                new String[]{Integer.toString(mSubscriptionId)});
+    }
+
+    /**
+     * getThreadId
+     * utilize the originator and recipients to obtain the thread id
+     */
+    private long getThreadId(Bmessage message) {
+
+        Set<String> messageContacts = new ArraySet<>();
+        String originator = PhoneNumberUtils.extractNetworkPortion(getOriginatorNumber(message));
+        if (originator != null) {
+            messageContacts.add(originator);
+        }
+        getRecipientsFromMessage(message, messageContacts);
+
+        messageContacts.remove(mPhoneNumber);
+        logV("Contacts = " + messageContacts.toString());
+        return Telephony.Threads.getOrCreateThreadId(mContext, messageContacts);
+    }
+
+    private void getRecipientsFromMessage(Bmessage message, Set<String> messageContacts) {
+        List<VCardEntry> recipients = message.getRecipients();
+        for (VCardEntry recipient : recipients) {
+            List<VCardEntry.PhoneData> phoneData = recipient.getPhoneList();
+            if (phoneData != null && phoneData.size() > 0) {
+                messageContacts
+                        .add(PhoneNumberUtils.extractNetworkPortion(phoneData.get(0).getNumber()));
+            }
+        }
+    }
+
+    private String getOriginatorNumber(Bmessage message) {
+        VCardEntry originator = message.getOriginator();
+        if (originator != null) {
+            List<VCardEntry.PhoneData> phoneData = originator.getPhoneList();
+            if (phoneData != null && phoneData.size() > 0) {
+                return phoneData.get(0).getNumber();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * addThreadContactToEntries
+     * utilizing the thread id fill in the appropriate fields of bmsg with the intended recipients
+     */
+    boolean addThreadContactsToEntries(Bmessage bmsg, String thread) {
+        String threadId = Uri.parse(thread).getLastPathSegment();
+
+        logD("MATCHING THREAD" + threadId);
+
+        Cursor cursor = mResolver
+                .query(Uri.withAppendedPath(MmsSms.CONTENT_CONVERSATIONS_URI,
+                        threadId + "/recipients"),
+                        null, null,
+                        null, null);
+
+        if (cursor.moveToNext()) {
+            logD("Columns" + Arrays.toString(cursor.getColumnNames()));
+            logV("CONTACT LIST: " + cursor.getString(cursor.getColumnIndex("recipient_ids")));
+            addRecipientsToEntries(bmsg,
+                    cursor.getString(cursor.getColumnIndex("recipient_ids")).split(" "));
+            return true;
+        } else {
+            Log.w(TAG, "Thread Not Found");
+            return false;
+        }
+    }
+
+
+    private void addRecipientsToEntries(Bmessage bmsg, String[] recipients) {
+        logV("CONTACT LIST: " + Arrays.toString(recipients));
+        for (String recipient : recipients) {
+            Cursor cursor = mResolver
+                    .query(Uri.parse("content://mms-sms/canonical-address/" + recipient), null,
+                            null, null,
+                            null);
+            while (cursor.moveToNext()) {
+                String number = cursor.getString(cursor.getColumnIndex(Mms.Addr.ADDRESS));
+                logV("CONTACT number: " + number);
+                VCardEntry destEntry = new VCardEntry();
+                VCardProperty destEntryPhone = new VCardProperty();
+                destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
+                destEntryPhone.addValues(number);
+                destEntry.addProperty(destEntryPhone);
+                bmsg.addRecipient(destEntry);
+            }
+        }
+    }
+
+    /**
+     * MessageStatus
+     *
+     * Helper class to store associations between remote and local provider based on message handle
+     * and read status
+     */
+    class MessageStatus {
+
+        String mHandle;
+        int mRead;
+
+        MessageStatus(String handle, int read) {
+            mHandle = handle;
+            mRead = read;
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            return ((other instanceof MessageStatus) && ((MessageStatus) other).mHandle
+                    .equals(mHandle));
+        }
+    }
+}
diff --git a/src/com/android/bluetooth/mapclient/MapClientService.java b/src/com/android/bluetooth/mapclient/MapClientService.java
index 3c056da..d1bf260 100644
--- a/src/com/android/bluetooth/mapclient/MapClientService.java
+++ b/src/com/android/bluetooth/mapclient/MapClientService.java
@@ -456,8 +456,8 @@
         }
 
         private MapClientService getService() {
-            if (!Utils.checkCaller()) {
-                Log.w(TAG, "MAP call not allowed for non-active user");
+            if (!Utils.checkCaller() && !MapUtils.isSystemUser()) {
+                Log.w(TAG, "MAP call not allowed for non-active and non-system user.");
                 return null;
             }
 
diff --git a/src/com/android/bluetooth/mapclient/MapUtils.java b/src/com/android/bluetooth/mapclient/MapUtils.java
index c47526a..a26816d 100644
--- a/src/com/android/bluetooth/mapclient/MapUtils.java
+++ b/src/com/android/bluetooth/mapclient/MapUtils.java
@@ -16,6 +16,7 @@
 package com.android.bluetooth.mapclient;
 
 import android.os.SystemProperties;
+import android.os.UserHandle;
 
 import com.android.bluetooth.Utils;
 import com.android.internal.annotations.VisibleForTesting;
@@ -32,6 +33,10 @@
         sMnsService = service;
     }
 
+    static boolean isSystemUser() {
+        return UserHandle.getCallingUserId() == UserHandle.USER_SYSTEM;
+    }
+
     static MnsService newMnsServiceInstance(MapClientService mapClientService) {
         return (sMnsService == null) ? new MnsService(mapClientService) : sMnsService;
     }
diff --git a/src/com/android/bluetooth/mapclient/MceStateMachine.java b/src/com/android/bluetooth/mapclient/MceStateMachine.java
index 5395595..1ca32d8 100644
--- a/src/com/android/bluetooth/mapclient/MceStateMachine.java
+++ b/src/com/android/bluetooth/mapclient/MceStateMachine.java
@@ -81,7 +81,7 @@
  * a connection to the Message Access Server is created and a request to enable notification of new
  * messages is sent.
  */
-final class MceStateMachine extends StateMachine {
+class MceStateMachine extends StateMachine {
     // Messages for events handled by the StateMachine
     static final int MSG_MAS_CONNECTED = 1001;
     static final int MSG_MAS_DISCONNECTED = 1002;
@@ -110,6 +110,7 @@
     private static final String FOLDER_MSG = "msg";
     private static final String FOLDER_OUTBOX = "outbox";
     private static final String FOLDER_INBOX = "inbox";
+    private static final String FOLDER_SENT = "sent";
     private static final String INBOX_PATH = "telecom/msg/inbox";
 
 
@@ -123,6 +124,7 @@
     private final BluetoothDevice mDevice;
     private MapClientService mService;
     private MasClient mMasClient;
+    private final MapClientContent mDatabase;
     private HashMap<String, Bmessage> mSentMessageLog = new HashMap<>(MAX_MESSAGES);
     private HashMap<Bmessage, PendingIntent> mSentReceiptRequested = new HashMap<>(MAX_MESSAGES);
     private HashMap<Bmessage, PendingIntent> mDeliveryReceiptRequested =
@@ -189,6 +191,14 @@
         mDisconnecting = new Disconnecting();
         mConnected = new Connected();
 
+        MapClientContent.Callbacks callbacks = new MapClientContent.Callbacks(){
+            @Override
+            public void onMessageStatusChanged(String handle, int status) {
+                setMessageStatus(handle, status);
+            }
+        };
+        mDatabase = new MapClientContent(mService, callbacks, mDevice);
+
         addState(mDisconnected);
         addState(mConnecting);
         addState(mDisconnecting);
@@ -272,16 +282,22 @@
 
             for (Uri contact : contacts) {
                 // Who to send the message to.
-                VCardEntry destEntry = new VCardEntry();
-                VCardProperty destEntryPhone = new VCardProperty();
                 if (DBG) {
                     Log.d(TAG, "Scheme " + contact.getScheme());
                 }
                 if (PhoneAccount.SCHEME_TEL.equals(contact.getScheme())) {
-                    destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
-                    destEntryPhone.addValues(contact.getSchemeSpecificPart());
-                    if (DBG) {
-                        Log.d(TAG, "Sending to phone numbers " + destEntryPhone.getValueList());
+                    if (contact.getPath().contains(Telephony.Threads.CONTENT_URI.toString())) {
+                        mDatabase.addThreadContactsToEntries(bmsg, contact.getLastPathSegment());
+                    } else {
+                        VCardEntry destEntry = new VCardEntry();
+                        VCardProperty destEntryPhone = new VCardProperty();
+                        destEntryPhone.setName(VCardConstants.PROPERTY_TEL);
+                        destEntryPhone.addValues(contact.getSchemeSpecificPart());
+                        destEntry.addProperty(destEntryPhone);
+                        bmsg.addRecipient(destEntry);
+                        if (DBG) {
+                            Log.d(TAG, "Sending to phone numbers " + destEntryPhone.getValueList());
+                        }
                     }
                 } else {
                     if (DBG) {
@@ -289,8 +305,6 @@
                     }
                     return false;
                 }
-                destEntry.addProperty(destEntryPhone);
-                bmsg.addRecipient(destEntry);
             }
 
             // Message of the body.
@@ -511,6 +525,8 @@
             mMasClient.makeRequest(new RequestGetFolderListing(0, 0));
             mMasClient.makeRequest(new RequestSetPath(false));
             mMasClient.makeRequest(new RequestSetNotificationRegistration(true));
+            sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_SENT);
+            sendMessage(MSG_GET_MESSAGE_LISTING, FOLDER_INBOX);
         }
 
         @Override
@@ -551,7 +567,6 @@
                     // Get latest 50 Unread messages in the last week
                     MessagesFilter filter = new MessagesFilter();
                     filter.setMessageType(MapUtils.fetchMessageType());
-                    filter.setReadStatus(MessagesFilter.READ_STATUS_UNREAD);
                     Calendar calendar = Calendar.getInstance();
                     calendar.add(Calendar.DATE, -7);
                     filter.setPeriod(calendar.getTime(), null);
@@ -607,6 +622,7 @@
 
         @Override
         public void exit() {
+            mDatabase.clearMessages();
             mPreviousState = BluetoothProfile.STATE_CONNECTED;
         }
 
@@ -636,7 +652,6 @@
                                 + ", Message handle = " + ev.getHandle());
                     }
                     switch (ev.getType()) {
-
                         case NEW_MESSAGE:
                             // Infer the timestamp for this message as 'now' and read status false
                             // instead of getting the message listing data for it
@@ -649,18 +664,23 @@
                             mMasClient.makeRequest(new RequestGetMessage(ev.getHandle(),
                                     MasClient.CharsetType.UTF_8, false));
                             break;
-
                         case DELIVERY_SUCCESS:
                         case SENDING_SUCCESS:
                             notifySentMessageStatus(ev.getHandle(), ev.getType());
                             break;
+                        case READ_STATUS_CHANGED:
+                            mDatabase.markRead(ev.getHandle());
+                            break;
+                        case MESSAGE_DELETED:
+                            mDatabase.deleteMessage(ev.getHandle());
+                            break;
                     }
             }
         }
 
         // Sets the specified message status to "read" (from "unread" status, mostly)
         private void markMessageRead(RequestGetMessage request) {
-            if (DBG) Log.d(TAG, "markMessageRead");
+            if (DBG) Log.d(TAG, "markMessageRead" + request.getHandle());
             MessageMetadata metadata = mMessages.get(request.getHandle());
             metadata.setRead(true);
             mMasClient.makeRequest(new RequestSetMessageStatus(request.getHandle(),
@@ -754,6 +774,8 @@
             if (message == null) {
                 return;
             }
+            mDatabase.storeMessage(message, request.getHandle(),
+                    mMessages.get(request.getHandle()).getTimestamp());
             if (!INBOX_PATH.equalsIgnoreCase(message.getFolder())) {
                 if (DBG) {
                     Log.d(TAG, "Ignoring message received in " + message.getFolder() + ".");
diff --git a/tests/unit/src/com/android/bluetooth/mapclient/MapClientContentTest.java b/tests/unit/src/com/android/bluetooth/mapclient/MapClientContentTest.java
new file mode 100644
index 0000000..0229038
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/mapclient/MapClientContentTest.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2017 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.bluetooth.mapclient;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothMapClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.telephony.SubscriptionManager;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.btservice.AdapterService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
+import com.android.vcard.VCardConstants;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardProperty;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class MapClientContentTest {
+
+    private static final String TAG = "MapClientContentTest";
+    private static final int READ = 1;
+
+    private BluetoothAdapter mAdapter;
+    private BluetoothDevice mTestDevice;
+    private Context mTargetContext;
+
+    private Handler mHandler;
+    private Bmessage mTestMessage1;
+    private Bmessage mTestMessage2;
+    private Long mTestMessage1Timestamp = 1234L;
+    private String mTestMessage1Handle = "0001";
+    private String mTestMessage2Handle = "0002";
+
+
+    private VCardEntry mOriginator;
+
+    private ArgumentCaptor<Uri> mUriArgument = ArgumentCaptor.forClass(Uri.class);
+
+    private MapClientContent mMapClientContent;
+
+    @Mock
+    private AdapterService mAdapterService;
+    @Mock
+    private DatabaseManager mDatabaseManager;
+    @Mock
+    private MapClientService mMockMapClientService;
+    @Mock
+    private Context mMockContext;
+    @Mock
+    private MapClientContent.Callbacks mCallbacks;
+
+    private MockContentResolver mMockContentResolver;
+    private FakeContentProvider mMockSmsContentProvider;
+    private FakeContentProvider mMockMmsContentProvider;
+    private FakeContentProvider mMockThreadContentProvider;
+
+    @Mock
+    private SubscriptionManager mMockSubscriptionManager;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+
+        mMockSmsContentProvider = Mockito.spy(new FakeContentProvider(mTargetContext));
+
+        mMockMmsContentProvider = Mockito.spy(new FakeContentProvider(mTargetContext));
+        mMockThreadContentProvider = Mockito.spy(new FakeContentProvider(mTargetContext));
+
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
+        mMockContentResolver = Mockito.spy(new MockContentResolver());
+        mMockContentResolver.addProvider("sms", mMockSmsContentProvider);
+        mMockContentResolver.addProvider("mms", mMockMmsContentProvider);
+        mMockContentResolver.addProvider("mms-sms", mMockThreadContentProvider);
+
+        when(mMockContext.getContentResolver()).thenReturn(mMockContentResolver);
+        when(mMockContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE))
+                .thenReturn(mMockSubscriptionManager);
+        createTestMessages();
+
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    /**
+     * Test that everything initializes correctly with an empty content provider
+     */
+    @Test
+    public void testCreateMapClientContent() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(0, mMockSmsContentProvider.mContentValues.size());
+    }
+
+    /**
+     * Test that a dirty database gets cleaned at startup.
+     */
+    @Test
+    public void testCleanDirtyDatabase() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage1, mTestMessage1Handle, mTestMessage1Timestamp);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(1, mMockSmsContentProvider.mContentValues.size());
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        Assert.assertEquals(0, mMockSmsContentProvider.mContentValues.size());
+    }
+
+    /**
+     * Test inserting 2 SMS messages and then clearing out the database.
+     */
+    @Test
+    public void testStoreTwoSMS() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage1, mTestMessage1Handle, mTestMessage1Timestamp);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(1, mMockSmsContentProvider.mContentValues.size());
+
+        mMapClientContent.storeMessage(mTestMessage1, mTestMessage1Handle, mTestMessage1Timestamp);
+        Assert.assertEquals(2, mMockSmsContentProvider.mContentValues.size());
+        Assert.assertEquals(0, mMockMmsContentProvider.mContentValues.size());
+
+        mMapClientContent.clearMessages();
+        Assert.assertEquals(0, mMockSmsContentProvider.mContentValues.size());
+    }
+
+    /**
+     * Test inserting 2 MMS messages and then clearing out the database.
+     */
+    @Test
+    public void testStoreTwoMMS() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage1Handle, mTestMessage1Timestamp);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(1, mMockMmsContentProvider.mContentValues.size());
+
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage1Handle, mTestMessage1Timestamp);
+        Assert.assertEquals(2, mMockMmsContentProvider.mContentValues.size());
+
+        mMapClientContent.clearMessages();
+        Assert.assertEquals(0, mMockMmsContentProvider.mContentValues.size());
+    }
+
+    /**
+     * Test that SMS and MMS messages end up in their respective databases.
+     */
+    @Test
+    public void testStoreOneSMSOneMMS() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage1Handle, mTestMessage1Timestamp);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(1, mMockMmsContentProvider.mContentValues.size());
+
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage2Handle, mTestMessage1Timestamp);
+        Assert.assertEquals(2, mMockMmsContentProvider.mContentValues.size());
+
+        mMapClientContent.clearMessages();
+        Assert.assertEquals(0, mMockMmsContentProvider.mContentValues.size());
+    }
+
+    /**
+     * Test read status changed
+     */
+    @Test
+    public void testReadStatusChanged() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage1Handle, mTestMessage1Timestamp);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(1, mMockMmsContentProvider.mContentValues.size());
+
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage1Handle, mTestMessage1Timestamp);
+        Assert.assertEquals(2, mMockMmsContentProvider.mContentValues.size());
+
+        mMapClientContent.markRead(mTestMessage1Handle);
+
+        mMapClientContent.clearMessages();
+        Assert.assertEquals(0, mMockMmsContentProvider.mContentValues.size());
+    }
+
+    /**
+     * Test read status changed in local provider
+     *
+     * Insert a message, and notify the observer about a change
+     * The cursor is configured to return messages marked as read
+     * Verify that the local change is observed and propagated to the remote
+     */
+    @Test
+    public void testLocalReadStatusChanged() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage1Handle, mTestMessage1Timestamp);
+        Assert.assertEquals(1, mMockMmsContentProvider.mContentValues.size());
+        mMapClientContent.mContentObserver.onChange(false);
+        verify(mCallbacks).onMessageStatusChanged(eq(mTestMessage1Handle),
+                eq(BluetoothMapClient.READ));
+    }
+
+    /**
+     * Test remote message deleted
+     *
+     * Add a message to the database Simulate the message getting
+     * deleted on the phone Verify that the message is deleted locally
+     */
+    @Test
+    public void testMessageDeleted() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage1, mTestMessage1Handle, mTestMessage1Timestamp);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(1, mMockSmsContentProvider.mContentValues.size());
+        // attempt to delete an invalid handle, nothing should be removed.
+        mMapClientContent.deleteMessage(mTestMessage2Handle);
+        Assert.assertEquals(1, mMockSmsContentProvider.mContentValues.size());
+
+        // delete a valid handle
+        mMapClientContent.deleteMessage(mTestMessage1Handle);
+        Assert.assertEquals(0, mMockSmsContentProvider.mContentValues.size());
+    }
+
+    /**
+     * Test read status changed in local provider
+     *
+     * Insert a message, manually remove it and notify the observer about a change
+     * Verify that the local change is observed and propagated to the remote
+     */
+    @Test
+    public void testLocalMessageDeleted() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        mMapClientContent.storeMessage(mTestMessage1, mTestMessage1Handle, mTestMessage1Timestamp);
+        verify(mMockSubscriptionManager).addSubscriptionInfoRecord(any(), any(), anyInt(),
+                eq(SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM));
+        Assert.assertEquals(1, mMockSmsContentProvider.mContentValues.size());
+        mMockSmsContentProvider.mContentValues.clear();
+        mMapClientContent.mContentObserver.onChange(false);
+        verify(mCallbacks).onMessageStatusChanged(eq(mTestMessage1Handle),
+                eq(BluetoothMapClient.DELETED));
+    }
+
+    /**
+     * Test parse own phone number Attempt to parse your phone number from a received SMS message
+     * and fail Receive an MMS message and successfully parse your phone number
+     */
+    @Test
+    public void testParseNumber() {
+        mMapClientContent = new MapClientContent(mMockContext, mCallbacks, mTestDevice);
+        Assert.assertNull(mMapClientContent.mPhoneNumber);
+        mMapClientContent.storeMessage(mTestMessage1, mTestMessage1Handle, mTestMessage1Timestamp);
+        Assert.assertNull(mMapClientContent.mPhoneNumber);
+        mMapClientContent.storeMessage(mTestMessage2, mTestMessage1Handle, mTestMessage1Timestamp);
+        Assert.assertEquals("5551212", mMapClientContent.mPhoneNumber);
+    }
+
+    void createTestMessages() {
+        mOriginator = new VCardEntry();
+        VCardProperty property = new VCardProperty();
+        property.setName(VCardConstants.PROPERTY_TEL);
+        property.addValues("555-1212");
+        mOriginator.addProperty(property);
+        mTestMessage1 = new Bmessage();
+        mTestMessage1.setBodyContent("HelloWorld");
+        mTestMessage1.setType(Bmessage.Type.SMS_GSM);
+        mTestMessage1.setFolder("telecom/msg/inbox");
+        mTestMessage1.addOriginator(mOriginator);
+
+        mTestMessage2 = new Bmessage();
+        mTestMessage2.setBodyContent("HelloWorld");
+        mTestMessage2.setType(Bmessage.Type.MMS);
+        mTestMessage2.setFolder("telecom/msg/inbox");
+        mTestMessage2.addOriginator(mOriginator);
+        mTestMessage2.addRecipient(mOriginator);
+    }
+
+    public class FakeContentProvider extends MockContentProvider {
+
+        Map<Uri, ContentValues> mContentValues = new HashMap<>();
+        FakeContentProvider(Context context) {
+            super(context);
+        }
+
+        @Override
+        public int delete(Uri uri, String selection, String[] selectionArgs) {
+            Log.i(TAG, "Delete " + uri);
+            Log.i(TAG, "Contents" + mContentValues.toString());
+            mContentValues.remove(uri);
+            if (uri.toString().equals("content://sms") || uri.toString().equals("content://mms")) {
+
+                mContentValues.clear();
+            }
+            return 1;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            Log.i(TAG, "URI = " + uri);
+            Uri returnUri = Uri.withAppendedPath(uri, String.valueOf(mContentValues.size() + 1));
+            //only store top level message parts
+            if (uri.toString().equals("content://sms/inbox") || uri.toString()
+                    .equals("content://mms")) {
+                Log.i(TAG, "adding content" + values);
+                mContentValues.put(returnUri, values);
+                Log.i(TAG, "ContentSize = " + mContentValues.size());
+            }
+            return returnUri;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                String sortOrder) {
+            Cursor cursor = Mockito.mock(Cursor.class);
+
+            when(cursor.moveToFirst()).thenReturn(true);
+            when(cursor.moveToNext()).thenReturn(true).thenReturn(false);
+
+            when(cursor.getLong(anyInt())).thenReturn((long) mContentValues.size());
+            when(cursor.getString(anyInt())).thenReturn(String.valueOf(mContentValues.size()));
+            when(cursor.getInt(anyInt())).thenReturn(READ);
+            return cursor;
+        }
+    }
+
+
+}
diff --git a/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java b/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java
index 972ad47..b5da1a2 100644
--- a/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/mapclient/MapClientStateMachineTest.java
@@ -25,22 +25,31 @@
 import android.bluetooth.SdpMasRecord;
 import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.telephony.SubscriptionManager;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
+import androidx.test.rule.ServiceTestRule;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+import com.android.bluetooth.btservice.AdapterService;
 import com.android.bluetooth.btservice.ProfileService;
+import com.android.bluetooth.btservice.storage.DatabaseManager;
 
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -50,31 +59,56 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class MapClientStateMachineTest {
+
     private static final String TAG = "MapStateMachineTest";
 
     private static final int ASYNC_CALL_TIMEOUT_MILLIS = 100;
-
+    @Rule
+    public final ServiceTestRule mServiceRule = new ServiceTestRule();
     private BluetoothAdapter mAdapter;
     private MceStateMachine mMceStateMachine = null;
     private BluetoothDevice mTestDevice;
     private Context mTargetContext;
-
     private Handler mHandler;
-
     private ArgumentCaptor<Intent> mIntentArgument = ArgumentCaptor.forClass(Intent.class);
-
+    @Mock
+    private AdapterService mAdapterService;
+    @Mock
+    private DatabaseManager mDatabaseManager;
     @Mock
     private MapClientService mMockMapClientService;
-
+    private MockContentResolver mMockContentResolver;
+    private MockContentProvider mMockContentProvider;
     @Mock
     private MasClient mMockMasClient;
 
+    @Mock
+    private SubscriptionManager mMockSubscriptionManager;
+
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mTargetContext = InstrumentationRegistry.getTargetContext();
+        mMockContentProvider = new MockContentProvider(mTargetContext) {
+            @Override
+            public int delete(Uri uri, String selection, String[] selectionArgs) {
+                return 0;
+            }
+        };
+        mMockContentResolver = new MockContentResolver();
+
         Assume.assumeTrue("Ignore test when MapClientService is not enabled",
                 mTargetContext.getResources().getBoolean(R.bool.profile_supported_mapmce));
+        TestUtils.setAdapterService(mAdapterService);
+        when(mAdapterService.getDatabase()).thenReturn(mDatabaseManager);
+        TestUtils.startService(mServiceRule, MapClientService.class);
+        mMockContentResolver.addProvider("sms", mMockContentProvider);
+        mMockContentResolver.addProvider("mms", mMockContentProvider);
+        mMockContentResolver.addProvider("mms-sms", mMockContentProvider);
+
+        when(mMockMapClientService.getContentResolver()).thenReturn(mMockContentResolver);
+        when(mMockMapClientService.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE))
+                .thenReturn(mMockSubscriptionManager);
 
         doReturn(mTargetContext.getResources()).when(mMockMapClientService).getResources();
 
@@ -93,13 +127,16 @@
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_mapmce)) {
             return;
         }
+
         if (mMceStateMachine != null) {
             mMceStateMachine.doQuit();
         }
+        TestUtils.stopService(mServiceRule, MapClientService.class);
+        TestUtils.clearAdapterService(mAdapterService);
     }
 
     /**
@@ -112,8 +149,8 @@
     }
 
     /**
-     * Test transition from
-     *      STATE_CONNECTING --> (receive MSG_MAS_DISCONNECTED) --> STATE_DISCONNECTED
+     * Test transition from STATE_CONNECTING --> (receive MSG_MAS_DISCONNECTED) -->
+     * STATE_DISCONNECTED
      */
     @Test
     public void testStateTransitionFromConnectingToDisconnected() {
@@ -151,9 +188,9 @@
         Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
     }
 
-     /**
-     * Test transition from STATE_CONNECTING --> (receive MSG_MAS_CONNECTED) --> STATE_CONNECTED
-     * --> (receive MSG_MAS_DISCONNECTED) --> STATE_DISCONNECTED
+    /**
+     * Test transition from STATE_CONNECTING --> (receive MSG_MAS_CONNECTED) --> STATE_CONNECTED -->
+     * (receive MSG_MAS_DISCONNECTED) --> STATE_DISCONNECTED
      */
     @Test
     public void testStateTransitionFromConnectedWithMasDisconnected() {
@@ -220,7 +257,8 @@
                 timeout(ASYNC_CALL_TIMEOUT_MILLIS).times(2)).sendBroadcast(
                 mIntentArgument.capture(), eq(ProfileService.BLUETOOTH_PERM));
         Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, mMceStateMachine.getState());
-        Assert.assertTrue(mMceStateMachine.setMessageStatus("123456789AB", BluetoothMapClient.READ));
+        Assert.assertTrue(
+                mMceStateMachine.setMessageStatus("123456789AB", BluetoothMapClient.READ));
     }
 
     private void setupSdpRecordReceipt() {