Implemention of the telephony provider backup agent to cover SMS backup

Known issues:
- Are we ok with restarting com.android.phone on restore?

Bug: 26079978
Bug: 26532098
Change-Id: I87452c0d993c14fe70b12f310bdc52a0ccd2ff1d
diff --git a/Android.mk b/Android.mk
index 7b63072..c824fff 100644
--- a/Android.mk
+++ b/Android.mk
@@ -5,7 +5,7 @@
 
 LOCAL_PRIVILEGED_MODULE := true
 
-LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
 
 LOCAL_PACKAGE_NAME := TelephonyProvider
 LOCAL_CERTIFICATE := platform
@@ -14,3 +14,5 @@
 LOCAL_STATIC_JAVA_LIBRARIES += android-common
 
 include $(BUILD_PACKAGE)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 49c71eb..256c347 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -29,7 +29,9 @@
 
     <application android:process="com.android.phone"
                  android:allowClearUserData="false"
-                 android:allowBackup="false"
+                 android:fullBackupOnly="true"
+                 android:backupAgent="TelephonyBackupAgent"
+                 android:restoreAnyVersion="true"
                  android:label="@string/app_label"
                  android:icon="@mipmap/ic_launcher_phone"
                  android:usesCleartextTraffic="true"
diff --git a/src/com/android/providers/telephony/TelephonyBackupAgent.java b/src/com/android/providers/telephony/TelephonyBackupAgent.java
new file mode 100644
index 0000000..50a7c8e
--- /dev/null
+++ b/src/com/android/providers/telephony/TelephonyBackupAgent.java
@@ -0,0 +1,922 @@
+/*
+ * Copyright (C) 2016 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.providers.telephony;
+
+import com.google.android.mms.ContentType;
+import com.google.android.mms.pdu.CharacterSets;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import android.annotation.TargetApi;
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FullBackupDataOutput;
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.net.Uri;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.Telephony;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/***
+ * Backup agent for backup and restore SMS's and text MMS's.
+ *
+ * This backup agent stores SMS's into "sms_backup" file as a JSON array. Example below.
+ *  [{"self_phone":"+1234567891011","address":"+1234567891012","body":"Example sms",
+ *  "date":"1450893518140","date_sent":"1450893514000","status":"-1","type":"1"},
+ *  {"self_phone":"+1234567891011","address":"12345","body":"Example 2","date":"1451328022316",
+ *  "date_sent":"1451328018000","status":"-1","type":"1"}]
+ *
+ * Text MMS's are stored into "mms_backup" file as a JSON array. Example below.
+ *  [{"self_phone":"+1234567891011","date":"1451322716","date_sent":"0","m_type":"128","v":"18",
+ *  "msg_box":"2","mms_addresses":[{"type":137,"address":"+1234567891011","charset":106},
+ *  {"type":151,"address":"example@example.com","charset":106}],"mms_body":"Mms to email",
+ *  "mms_charset":106},
+ *  {"self_phone":"+1234567891011","sub":"MMS subject","date":"1451322955","date_sent":"0",
+ *  "m_type":"132","v":"17","msg_box":"1","ct_l":"http://promms/servlets/NOK5BBqgUHAqugrQNM",
+ *  "mms_addresses":[{"type":151,"address":"+1234567891011","charset":106}],
+ *  "mms_body":"Mms\nBody\r\n",
+ *  "mms_charset":106,"sub_cs":"106"}]
+ *
+ *   It deflates the files on the flight.
+ *   Every 1000 messages it backs up file, deletes it and creates a new one with the same name.
+ */
+
+@TargetApi(Build.VERSION_CODES.M)
+public class TelephonyBackupAgent extends BackupAgent {
+    private static final String TAG = "TelephonyBackupAgent";
+    private static final boolean DEBUG = false;
+
+
+    // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java.
+    private static final int DEFAULT_DURATION = 5000; //ms
+
+    // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java.
+    @VisibleForTesting
+    static final String sSmilTextOnly =
+            "<smil>" +
+                "<head>" +
+                    "<layout>" +
+                        "<root-layout/>" +
+                        "<region id=\"Text\" top=\"0\" left=\"0\" "
+                        + "height=\"100%%\" width=\"100%%\"/>" +
+                    "</layout>" +
+                "</head>" +
+                "<body>" +
+                       "%s" +  // constructed body goes here
+                "</body>" +
+            "</smil>";
+
+    // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java.
+    @VisibleForTesting
+    static final String sSmilTextPart =
+            "<par dur=\"" + DEFAULT_DURATION + "ms\">" +
+                "<text src=\"%s\" region=\"Text\" />" +
+            "</par>";
+
+
+    // JSON key for phone number a message was sent from or received to.
+    private static final String SELF_PHONE_KEY = "self_phone";
+    // JSON key for list of addresses of MMS message.
+    private static final String MMS_ADDRESSES_KEY = "mms_addresses";
+    // JSON key for MMS body.
+    private static final String MMS_BODY_KEY = "mms_body";
+    // JSON key for MMS charset.
+    private static final String MMS_BODY_CHARSET_KEY = "mms_charset";
+
+    // File names for backup/restore.
+    private static final String SMS_BACKUP_FILE = "sms_backup";
+    private static final String MMS_BACKUP_FILE = "mms_backup";
+
+    // Charset being used for reading/writing backup files.
+    private static final String CHARSET_UTF8 = "UTF-8";
+
+    // Order by ID entries from database.
+    private static final String ORDER_BY_ID = BaseColumns._ID + " ASC";
+
+    // Columns from SMS database for backup/restore.
+    @VisibleForTesting
+    static final String[] SMS_PROJECTION = new String[] {
+            Telephony.Sms._ID,
+            Telephony.Sms.SUBSCRIPTION_ID,
+            Telephony.Sms.ADDRESS,
+            Telephony.Sms.BODY,
+            Telephony.Sms.SUBJECT,
+            Telephony.Sms.DATE,
+            Telephony.Sms.DATE_SENT,
+            Telephony.Sms.STATUS,
+            Telephony.Sms.TYPE
+    };
+
+    // Columns from MMS database for backup/restore.
+    @VisibleForTesting
+    static final String[] MMS_PROJECTION = new String[] {
+            Telephony.Mms._ID,
+            Telephony.Mms.SUBSCRIPTION_ID,
+            Telephony.Mms.SUBJECT,
+            Telephony.Mms.SUBJECT_CHARSET,
+            Telephony.Mms.DATE,
+            Telephony.Mms.DATE_SENT,
+            Telephony.Mms.MESSAGE_TYPE,
+            Telephony.Mms.MMS_VERSION,
+            Telephony.Mms.TEXT_ONLY,
+            Telephony.Mms.MESSAGE_BOX,
+            Telephony.Mms.CONTENT_LOCATION
+    };
+
+    // Columns from addr database for backup/restore. This database is used for fetching addresses
+    // for MMS message.
+    @VisibleForTesting
+    static final String[] MMS_ADDR_PROJECTION = new String[] {
+            Telephony.Mms.Addr.TYPE,
+            Telephony.Mms.Addr.ADDRESS,
+            Telephony.Mms.Addr.CHARSET
+    };
+
+    // Columns from part database for backup/restore. This database is used for fetching body text
+    // and charset for MMS message.
+    @VisibleForTesting
+    static final String[] MMS_TEXT_PROJECTION = new String[] {
+            Telephony.Mms.Part.CONTENT_TYPE,
+            Telephony.Mms.Part.TEXT,
+            Telephony.Mms.Part.CHARSET
+    };
+
+    // Maximum messages for one backup file. After reaching the limit the agent backs up the file,
+    // deletes it and creates a new one with the same name.
+    private static final int MAX_MSG_PER_FILE = 1000;
+
+
+    // Default values for SMS, MMS, Addresses restore.
+    private static final ContentValues defaultValuesSms = new ContentValues(3);
+    private static final ContentValues defaultValuesMms = new ContentValues(5);
+    private static final ContentValues defaultValuesAddr = new ContentValues(2);
+
+    static {
+        // Consider restored messages read and seen.
+        defaultValuesSms.put(Telephony.Sms.READ, 1);
+        defaultValuesSms.put(Telephony.Sms.SEEN, 1);
+        // If there is no sub_id with self phone number on restore set it to -1.
+        defaultValuesSms.put(Telephony.Sms.SUBSCRIPTION_ID, -1);
+
+        defaultValuesMms.put(Telephony.Mms.READ, 1);
+        defaultValuesMms.put(Telephony.Mms.SEEN, 1);
+        defaultValuesMms.put(Telephony.Mms.SUBSCRIPTION_ID, -1);
+        defaultValuesMms.put(Telephony.Mms.MESSAGE_BOX, Telephony.Mms.MESSAGE_BOX_ALL);
+        defaultValuesMms.put(Telephony.Mms.TEXT_ONLY, 1);
+
+        defaultValuesAddr.put(Telephony.Mms.Addr.TYPE, 0);
+        defaultValuesAddr.put(Telephony.Mms.Addr.CHARSET, CharacterSets.DEFAULT_CHARSET);
+    }
+
+
+    private SparseArray<String> subId2phone;
+    private Map<String, Integer> phone2subId;
+    private SmsProvider mSmsProvider;
+    private MmsProvider mMmsProvider;
+    private MmsSmsProvider mMmsSmsProvider;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        subId2phone = new SparseArray<String>();
+        phone2subId = new ArrayMap<String, Integer>();
+        final SubscriptionManager subscriptionManager = SubscriptionManager.from(this);
+        if (subscriptionManager != null) {
+            final List<SubscriptionInfo> subInfo =
+                    subscriptionManager.getActiveSubscriptionInfoList();
+            if (subInfo != null) {
+                for (SubscriptionInfo sub : subInfo) {
+                    final String phoneNumber = getNormalizedNumber(sub);
+                    subId2phone.append(sub.getSubscriptionId(), phoneNumber);
+                    phone2subId.put(phoneNumber, sub.getSubscriptionId());
+                }
+            }
+        }
+
+        mSmsProvider = new SmsProvider();
+        mSmsProvider.attachInfo(this, null);
+        mSmsProvider.onCreate();
+
+        mMmsProvider = new MmsProvider();
+        mMmsProvider.attachInfo(this, null);
+        mMmsProvider.onCreate();
+
+        mMmsSmsProvider = new MmsSmsProvider();
+        mMmsSmsProvider.attachInfo(this, null);
+        mMmsSmsProvider.onCreate();
+    }
+
+
+    @Override
+    public void onFullBackup(FullBackupDataOutput data) throws IOException {
+        try (Cursor cursor = mSmsProvider.query(Telephony.Sms.CONTENT_URI, SMS_PROJECTION, null,
+                null, ORDER_BY_ID)) {
+            if (DEBUG) {
+                Log.i(TAG, "Backing up SMS");
+            }
+            if (cursor != null) {
+                while (!cursor.isLast() && !cursor.isAfterLast()) {
+                    try (JsonWriter jsonWriter = getJsonWriter(SMS_BACKUP_FILE)) {
+                        putSmsMessagesToJson(cursor, subId2phone, jsonWriter, MAX_MSG_PER_FILE);
+                    }
+                    backupFile(SMS_BACKUP_FILE, data);
+                }
+            }
+        }
+
+        try (Cursor cursor = mMmsProvider.query(Telephony.Mms.CONTENT_URI, MMS_PROJECTION, null,
+                null, ORDER_BY_ID)) {
+            if (DEBUG) {
+                Log.i(TAG, "Backing up text MMS");
+            }
+            if (cursor != null) {
+                while (!cursor.isLast() && !cursor.isAfterLast()) {
+                    try (JsonWriter jsonWriter = getJsonWriter(MMS_BACKUP_FILE)) {
+                        putMmsMessagesToJson(cursor, mMmsProvider, subId2phone, jsonWriter,
+                                MAX_MSG_PER_FILE);
+                    }
+                    backupFile(MMS_BACKUP_FILE, data);
+                }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    static void putMmsMessagesToJson(Cursor cursor, ContentProvider mmsProvider,
+                                     SparseArray<String> subId2phone, JsonWriter jsonWriter,
+                                     int maxMsgPerFile) throws IOException {
+        jsonWriter.beginArray();
+        for (int msgCount=0; msgCount<maxMsgPerFile && cursor.moveToNext();) {
+            msgCount += writeMmsToWriter(jsonWriter, cursor, subId2phone, mmsProvider);
+        }
+        jsonWriter.endArray();
+    }
+
+    @VisibleForTesting
+    static void putSmsMessagesToJson(Cursor cursor, SparseArray<String> subId2phone,
+                             JsonWriter jsonWriter, int maxMsgPerFile) throws IOException {
+
+        jsonWriter.beginArray();
+        for (int msgCount=0; msgCount<maxMsgPerFile && cursor.moveToNext(); ++msgCount) {
+            writeSmsToWriter(jsonWriter, cursor, subId2phone);
+        }
+        jsonWriter.endArray();
+    }
+
+    private void backupFile(String fileName, FullBackupDataOutput data) {
+        final File file = new File(getFilesDir().getPath() + "/" + fileName);
+        super.fullBackupFile(file, data);
+        file.delete();
+    }
+
+    @Override
+    public void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type,
+                              long mode, long mtime) throws IOException {
+        if (DEBUG) {
+            Log.i(TAG, "Restoring file " + destination.getName());
+        }
+
+        if (destination.getName().equals(SMS_BACKUP_FILE)) {
+            if (DEBUG) {
+                Log.i(TAG, "Restoring SMS");
+            }
+            try (JsonReader jsonReader = getJsonReader(data.getFileDescriptor())) {
+                putSmsMessagesToProvider(jsonReader, mSmsProvider, mMmsSmsProvider, phone2subId);
+            }
+        } else if (destination.getName().equals(MMS_BACKUP_FILE)) {
+            if (DEBUG) {
+                Log.i(TAG, "Restoring text MMS");
+            }
+            try (JsonReader jsonReader = getJsonReader(data.getFileDescriptor())) {
+                putMmsMessagesToProvider(jsonReader, mMmsProvider, mMmsSmsProvider, phone2subId);
+            }
+        } else {
+            super.onRestoreFile(data, size, destination, type, mode, mtime);
+        }
+        if (DEBUG) {
+            Log.i(TAG, "Finished restore");
+        }
+    }
+
+    @VisibleForTesting
+    static void putSmsMessagesToProvider(JsonReader jsonReader, ContentProvider smsProvider,
+                                         ContentProvider threadProvider,
+                                         Map<String, Integer> phone2subId) throws IOException {
+        jsonReader.beginArray();
+        while (jsonReader.hasNext()) {
+            ContentValues smsValues =
+                    readSmsValuesFromReader(jsonReader, threadProvider, phone2subId);
+            if (doesSmsExist(smsProvider, smsValues)) {
+                if (DEBUG) {
+                    Log.e(TAG, String.format("Sms: %s already exists", smsValues.toString()));
+                }
+                continue;
+            }
+            smsProvider.insert(Telephony.Sms.CONTENT_URI, smsValues);
+        }
+        jsonReader.endArray();
+    }
+
+    @VisibleForTesting
+    static void putMmsMessagesToProvider(JsonReader jsonReader, ContentProvider mmsProvider,
+                                         ContentProvider threadProvider,
+                                         Map<String, Integer> phone2subId) throws IOException {
+        jsonReader.beginArray();
+        while (jsonReader.hasNext()) {
+            final Mms mms = readMmsFromReader(jsonReader, threadProvider, phone2subId);
+            if (doesMmsExist(mmsProvider, mms)) {
+                if (DEBUG) {
+                    Log.e(TAG, String.format("Mms: %s already exists", mms.toString()));
+                }
+                continue;
+            }
+            addMmsMessage(mmsProvider, mms);
+        }
+    }
+
+    @VisibleForTesting
+    static final String[] PROJECTION_ID = {BaseColumns._ID};
+
+    private static boolean doesSmsExist(ContentProvider smsProvider, ContentValues smsValues) {
+        final String where = String.format("%s = %d and %s = %s",
+                Telephony.Sms.DATE, smsValues.getAsLong(Telephony.Sms.DATE),
+                Telephony.Sms.BODY,
+                DatabaseUtils.sqlEscapeString(smsValues.getAsString(Telephony.Sms.BODY)));
+        try (Cursor cursor = smsProvider.query(Telephony.Sms.CONTENT_URI, PROJECTION_ID, where,
+                null, null)) {
+            return cursor != null && cursor.getCount() > 0;
+        }
+    }
+
+    private static boolean doesMmsExist(ContentProvider mmsProvider, Mms mms) {
+        final String where = String.format("%s = %d",
+                Telephony.Sms.DATE, mms.values.getAsLong(Telephony.Mms.DATE));
+        try (Cursor cursor = mmsProvider.query(Telephony.Mms.CONTENT_URI, PROJECTION_ID, where,
+                null, null)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                do {
+                    final int mmsId = cursor.getInt(0);
+                    final MmsBody body = getMmsBody(mmsProvider, mmsId);
+                    if (body != null && body.equals(mms.body)) {
+                        return true;
+                    }
+                } while (cursor.moveToNext());
+            }
+        }
+        return false;
+    }
+
+    private static String getNormalizedNumber(SubscriptionInfo subscriptionInfo) {
+        if (subscriptionInfo == null) {
+            return null;
+        }
+        return PhoneNumberUtils.formatNumberToE164(subscriptionInfo.getNumber(),
+                subscriptionInfo.getCountryIso().toUpperCase(Locale.US));
+    }
+
+    private static void writeSmsToWriter(JsonWriter jsonWriter, Cursor cursor,
+                                         SparseArray<String> subId2phone) throws IOException {
+        jsonWriter.beginObject();
+
+        for (int i=0; i<cursor.getColumnCount(); ++i) {
+            final String name = cursor.getColumnName(i);
+            final String value = cursor.getString(i);
+            if (value == null) {
+                continue;
+            }
+            switch (name) {
+                case Telephony.Sms.SUBSCRIPTION_ID:
+                    final int subId = cursor.getInt(i);
+                    final String selfNumber = subId2phone.get(subId);
+                    if (selfNumber != null) {
+                        jsonWriter.name(SELF_PHONE_KEY).value(selfNumber);
+                    }
+                    break;
+                case Telephony.Sms._ID:
+                    break;
+                default:
+                    jsonWriter.name(name).value(value);
+                    break;
+            }
+        }
+        jsonWriter.endObject();
+    }
+
+    @VisibleForTesting
+    static ContentValues readSmsValuesFromReader(JsonReader jsonReader,
+                                                 ContentProvider threadProvider,
+                                                 Map<String, Integer> phone2id)
+            throws IOException {
+        ContentValues values = new ContentValues(8+defaultValuesSms.size());
+        values.putAll(defaultValuesSms);
+        jsonReader.beginObject();
+        while (jsonReader.hasNext()) {
+            String name = jsonReader.nextName();
+            switch (name) {
+                case Telephony.Sms.BODY:
+                case Telephony.Sms.DATE:
+                case Telephony.Sms.DATE_SENT:
+                case Telephony.Sms.STATUS:
+                case Telephony.Sms.TYPE:
+                case Telephony.Sms.SUBJECT:
+                    values.put(name, jsonReader.nextString());
+                    break;
+                case Telephony.Sms.ADDRESS:
+                    final String address = jsonReader.nextString();
+                    values.put(name, address);
+                    values.put(Telephony.Sms.THREAD_ID,
+                            getOrCreateThreadId(threadProvider, getSmsRecipients(address)));
+                    break;
+                case SELF_PHONE_KEY:
+                    final String selfPhone = jsonReader.nextString();
+                    if (phone2id.containsKey(selfPhone)) {
+                        values.put(Telephony.Sms.SUBSCRIPTION_ID, phone2id.get(selfPhone));
+                    }
+                    break;
+                default:
+                    if (DEBUG) {
+                        Log.w(TAG, "Unknown name:" + name);
+                    }
+                    jsonReader.skipValue();
+                    break;
+            }
+        }
+        jsonReader.endObject();
+        return values;
+    }
+
+    private static Set<String> getSmsRecipients(String address) {
+        Set<String> recipients = new ArraySet<String>();
+        recipients.addAll(Arrays.asList(address.split("[\\s,;]+")));
+        return recipients;
+    }
+
+    private static int writeMmsToWriter(JsonWriter jsonWriter, Cursor cursor,
+                                        SparseArray<String> subId2phone,
+                                        ContentProvider mmsProvider) throws IOException {
+        // Do not backup non text-only MMS's.
+        if (cursor.getInt(cursor.getColumnIndex(Telephony.Mms.TEXT_ONLY)) != 1) {
+            return 0;
+        }
+        final int mmsId = cursor.getInt(0);
+        final MmsBody body = getMmsBody(mmsProvider, mmsId);
+        if (body == null || body.text == null) {
+            return 0;
+        }
+
+        boolean subjectNull = true;
+        jsonWriter.beginObject();
+        for (int i=0; i<cursor.getColumnCount(); ++i) {
+            final String name = cursor.getColumnName(i);
+            final String value = cursor.getString(i);
+            if (value == null) {
+                continue;
+            }
+            switch (name) {
+                case Telephony.Sms.SUBSCRIPTION_ID:
+                    final int subId = cursor.getInt(i);
+                    final String selfNumber = subId2phone.get(subId);
+                    if (selfNumber != null) {
+                        jsonWriter.name(SELF_PHONE_KEY).value(selfNumber);
+                    }
+                    break;
+                case Telephony.Mms._ID:
+                case Telephony.Mms.TEXT_ONLY:
+                case Telephony.Mms.SUBJECT_CHARSET:
+                    break;
+                case Telephony.Mms.SUBJECT:
+                    subjectNull = false;
+                default:
+                    jsonWriter.name(name).value(value);
+                    break;
+            }
+        }
+        // Addresses.
+        writeMmsAddresses(jsonWriter.name(MMS_ADDRESSES_KEY), mmsProvider, mmsId);
+        // Body (text of the message).
+        jsonWriter.name(MMS_BODY_KEY).value(body.text);
+        // Charset of the body text.
+        jsonWriter.name(MMS_BODY_CHARSET_KEY).value(body.charSet);
+
+        if (!subjectNull) {
+            // Subject charset.
+            writeStringToWriter(jsonWriter, cursor, Telephony.Mms.SUBJECT_CHARSET);
+        }
+        jsonWriter.endObject();
+        return 1;
+    }
+
+    private static Mms readMmsFromReader(JsonReader jsonReader, ContentProvider threadProvider,
+                                         Map<String, Integer> phone2id) throws IOException {
+        Mms mms = new Mms();
+        mms.values = new ContentValues(6+defaultValuesMms.size());
+        mms.values.putAll(defaultValuesMms);
+        jsonReader.beginObject();
+        String selfPhone = null;
+        String bodyText = null;
+        int bodyCharset = CharacterSets.DEFAULT_CHARSET;
+        while (jsonReader.hasNext()) {
+            String name = jsonReader.nextName();
+            switch (name) {
+                case SELF_PHONE_KEY:
+                    selfPhone = jsonReader.nextString();
+                    if (phone2id.containsKey(selfPhone)) {
+                        mms.values.put(Telephony.Mms.SUBSCRIPTION_ID, phone2id.get(selfPhone));
+                    }
+                    break;
+                case MMS_ADDRESSES_KEY:
+                    getMmsAddressesFromReader(jsonReader, mms);
+                    break;
+                case MMS_BODY_KEY:
+                    bodyText = jsonReader.nextString();
+                    break;
+                case MMS_BODY_CHARSET_KEY:
+                    bodyCharset = jsonReader.nextInt();
+                    break;
+                case Telephony.Mms.SUBJECT:
+                case Telephony.Mms.SUBJECT_CHARSET:
+                case Telephony.Mms.DATE:
+                case Telephony.Mms.DATE_SENT:
+                case Telephony.Mms.MESSAGE_TYPE:
+                case Telephony.Mms.MMS_VERSION:
+                case Telephony.Mms.MESSAGE_BOX:
+                case Telephony.Mms.CONTENT_LOCATION:
+                    mms.values.put(name, jsonReader.nextString());
+                    break;
+                default:
+                    if (DEBUG) {
+                        Log.w(TAG, "Unknown name:" + name);
+                    }
+                    jsonReader.skipValue();
+                    break;
+            }
+        }
+        jsonReader.endObject();
+
+        if (bodyText != null) {
+            mms.body = new MmsBody(bodyText, bodyCharset);
+        }
+
+        { // Get ThreadId.
+            Set<String> recipients = new ArraySet<String>();
+            for (ContentValues mmsAddress : mms.addresses) {
+                String address = getDecodedString(
+                        getStringBytes(mmsAddress.getAsString(Telephony.Mms.Addr.ADDRESS),
+                                CharacterSets.ISO_8859_1),
+                        mmsAddress.getAsInteger(Telephony.Mms.Addr.CHARSET));
+                if (selfPhone != null && selfPhone.equals(address))
+                    continue;
+                recipients.add(address);
+            }
+            mms.values.put(Telephony.Mms.THREAD_ID,
+                    getOrCreateThreadId(threadProvider, recipients));
+        }
+
+        // Set default charset for subject.
+        if (mms.values.get(Telephony.Mms.SUBJECT) != null &&
+                mms.values.get(Telephony.Mms.SUBJECT_CHARSET) == null) {
+            mms.values.put(Telephony.Mms.SUBJECT_CHARSET, CharacterSets.DEFAULT_CHARSET);
+        }
+
+        return mms;
+    }
+
+    private static MmsBody getMmsBody(ContentProvider mmsProvider, int mmsId) {
+        Uri MMS_PART_CONTENT_URI = Telephony.Mms.CONTENT_URI.buildUpon()
+                .appendPath(String.valueOf(mmsId)).appendPath("part").build();
+
+        String body = null;
+        int charSet = 0;
+
+        try (Cursor cursor = mmsProvider.query(MMS_PART_CONTENT_URI, MMS_TEXT_PROJECTION,
+                null, null/*selectionArgs*/, ORDER_BY_ID)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                do {
+                    if (ContentType.TEXT_PLAIN.equals(cursor.getString(0))) {
+                        body = (body == null ? cursor.getString(1)
+                                             : body.concat(cursor.getString(1)));
+                        charSet = cursor.getInt(2);
+                    }
+                } while (cursor.moveToNext());
+            }
+        }
+        return (body == null ? null : new MmsBody(body, charSet));
+    }
+
+    private static void writeMmsAddresses(JsonWriter jsonWriter, ContentProvider mmsProvider,
+                                          int mmsId) throws IOException {
+        Uri.Builder builder = Telephony.Mms.CONTENT_URI.buildUpon();
+        builder.appendPath(String.valueOf(mmsId)).appendPath("addr");
+        Uri uriAddrPart = builder.build();
+
+        jsonWriter.beginArray();
+        try (Cursor cursor = mmsProvider.query(uriAddrPart, MMS_ADDR_PROJECTION,
+                null/*selection*/, null/*selectionArgs*/, ORDER_BY_ID)) {
+            if (cursor != null && cursor.moveToFirst()) {
+                do {
+                    if (cursor.getString(cursor.getColumnIndex(Telephony.Mms.Addr.ADDRESS))
+                            != null) {
+                        jsonWriter.beginObject();
+                        writeIntToWriter(jsonWriter, cursor, Telephony.Mms.Addr.TYPE);
+                        writeStringToWriter(jsonWriter, cursor, Telephony.Mms.Addr.ADDRESS);
+                        writeIntToWriter(jsonWriter, cursor, Telephony.Mms.Addr.CHARSET);
+                        jsonWriter.endObject();
+                    }
+                } while (cursor.moveToNext());
+            }
+        }
+        jsonWriter.endArray();
+    }
+
+    private static void getMmsAddressesFromReader(JsonReader jsonReader, Mms mms)
+            throws IOException {
+        mms.addresses = new ArrayList<ContentValues>();
+        jsonReader.beginArray();
+        while (jsonReader.hasNext()) {
+            jsonReader.beginObject();
+            ContentValues addrValues = new ContentValues(defaultValuesAddr);
+            while (jsonReader.hasNext()) {
+                final String name = jsonReader.nextName();
+                switch (name) {
+                    case Telephony.Mms.Addr.TYPE:
+                    case Telephony.Mms.Addr.CHARSET:
+                        addrValues.put(name, jsonReader.nextInt());
+                        break;
+                    case Telephony.Mms.Addr.ADDRESS:
+                        addrValues.put(name, jsonReader.nextString());
+                        break;
+                    default:
+                        if (DEBUG) {
+                            Log.w(TAG, "Unknown name:" + name);
+                        }
+                        jsonReader.skipValue();
+                        break;
+                }
+            }
+            jsonReader.endObject();
+            if (addrValues.containsKey(Telephony.Mms.Addr.ADDRESS)) {
+                mms.addresses.add(addrValues);
+            }
+        }
+        jsonReader.endArray();
+    }
+
+    private static void addMmsMessage(ContentProvider mmsProvider, Mms mms) {
+        if (DEBUG) {
+            Log.e(TAG, "Add mms:\n" + mms.toString());
+        }
+        final long dummyId = System.currentTimeMillis(); // Dummy ID of the msg.
+        final Uri partUri = Telephony.Mms.CONTENT_URI.buildUpon()
+                .appendPath(String.valueOf(dummyId)).appendPath("part").build();
+
+        final String srcName = String.format("text.%06d.txt", 0);
+        { // Insert SMIL part.
+            final String smilBody = String.format(sSmilTextPart, srcName);
+            final String smil = String.format(sSmilTextOnly, smilBody);
+            final ContentValues values = new ContentValues(7);
+            values.put(Telephony.Mms.Part.MSG_ID, dummyId);
+            values.put(Telephony.Mms.Part.SEQ, -1);
+            values.put(Telephony.Mms.Part.CONTENT_TYPE, ContentType.APP_SMIL);
+            values.put(Telephony.Mms.Part.NAME, "smil.xml");
+            values.put(Telephony.Mms.Part.CONTENT_ID, "<smil>");
+            values.put(Telephony.Mms.Part.CONTENT_LOCATION, "smil.xml");
+            values.put(Telephony.Mms.Part.TEXT, smil);
+            if (mmsProvider.insert(partUri, values) == null) {
+                if (DEBUG) {
+                    Log.e(TAG, "Could not insert SMIL part");
+                }
+                return;
+            }
+        }
+
+        { // Insert body part.
+            final ContentValues values = new ContentValues(8);
+            values.put(Telephony.Mms.Part.MSG_ID, dummyId);
+            values.put(Telephony.Mms.Part.SEQ, 0);
+            values.put(Telephony.Mms.Part.CONTENT_TYPE, ContentType.TEXT_PLAIN);
+            values.put(Telephony.Mms.Part.NAME, srcName);
+            values.put(Telephony.Mms.Part.CONTENT_ID, "<"+srcName+">");
+            values.put(Telephony.Mms.Part.CONTENT_LOCATION, srcName);
+            values.put(Telephony.Mms.Part.CHARSET, mms.body.charSet);
+            values.put(Telephony.Mms.Part.TEXT, mms.body.text);
+            if (mmsProvider.insert(partUri, values) == null) {
+                if (DEBUG) {
+                    Log.e(TAG, "Could not insert body part");
+                }
+                return;
+            }
+        }
+
+        // Insert mms.
+        final Uri mmsUri = mmsProvider.insert(Telephony.Mms.CONTENT_URI, mms.values);
+        if (mmsUri == null) {
+            if (DEBUG) {
+                Log.e(TAG, "Could not insert mms");
+            }
+            return;
+        }
+
+        final long mmsId = ContentUris.parseId(mmsUri);
+        { // Update parts with the right mms id.
+            ContentValues values = new ContentValues(1);
+            values.put(Telephony.Mms.Part.MSG_ID, mmsId);
+            mmsProvider.update(partUri, values, null, null);
+        }
+
+        { // Insert adderesses into "addr".
+            final Uri addrUri = Uri.withAppendedPath(mmsUri, "addr");
+            for (ContentValues mmsAddress : mms.addresses) {
+                ContentValues values = new ContentValues(mmsAddress);
+                values.put(Telephony.Mms.Addr.MSG_ID, mmsId);
+                mmsProvider.insert(addrUri, values);
+            }
+        }
+    }
+
+    private static final class MmsBody {
+        public String text;
+        public int charSet;
+
+        public MmsBody(String text, int charSet) {
+            this.text = text;
+            this.charSet = charSet;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null || !(obj instanceof MmsBody)) {
+                return false;
+            }
+            MmsBody typedObj = (MmsBody) obj;
+            return this.text.equals(typedObj.text) && this.charSet == typedObj.charSet;
+        }
+
+        @Override
+        public String toString() {
+            return "Text:" + text + " charSet:" + charSet;
+        }
+    }
+
+    private static final class Mms {
+        public ContentValues values;
+        public List<ContentValues> addresses;
+        public MmsBody body;
+        @Override
+        public String toString() {
+            return "Values:" + values.toString() + "\nRecipients:"+addresses.toString()
+                    + "\nBody:" + body;
+        }
+    }
+
+    private JsonWriter getJsonWriter(final String fileName) throws IOException {
+        return new JsonWriter(new OutputStreamWriter(new DeflaterOutputStream(
+                openFileOutput(fileName, MODE_PRIVATE)), CHARSET_UTF8));
+    }
+
+    private JsonReader getJsonReader(final FileDescriptor fileDescriptor) throws IOException {
+        return new JsonReader(new InputStreamReader(new InflaterInputStream(
+                new FileInputStream(fileDescriptor)), CHARSET_UTF8));
+    }
+
+    private static void writeStringToWriter(JsonWriter jsonWriter, Cursor cursor, String name)
+            throws IOException {
+        final String value = cursor.getString(cursor.getColumnIndex(name));
+        if (value != null) {
+            jsonWriter.name(name).value(value);
+        }
+    }
+
+    private static void writeIntToWriter(JsonWriter jsonWriter, Cursor cursor, String name)
+            throws IOException {
+        final int value = cursor.getInt(cursor.getColumnIndex(name));
+        if (value != 0) {
+            jsonWriter.name(name).value(value);
+        }
+    }
+
+    // Copied from packages/apps/Messaging/src/com/android/messaging/sms/DatabaseMessages.java.
+    /**
+     * Decoded string by character set
+     */
+    private static String getDecodedString(final byte[] data, final int charset)  {
+        if (CharacterSets.ANY_CHARSET == charset) {
+            return new String(data); // system default encoding.
+        } else {
+            try {
+                final String name = CharacterSets.getMimeName(charset);
+                return new String(data, name);
+            } catch (final UnsupportedEncodingException e) {
+                try {
+                    return new String(data, CharacterSets.MIMENAME_ISO_8859_1);
+                } catch (final UnsupportedEncodingException exception) {
+                    return new String(data); // system default encoding.
+                }
+            }
+        }
+    }
+
+    // Copied from packages/apps/Messaging/src/com/android/messaging/sms/DatabaseMessages.java.
+    /**
+     * Unpack a given String into a byte[].
+     */
+    private static byte[] getStringBytes(final String data, final int charset) {
+        if (CharacterSets.ANY_CHARSET == charset) {
+            return data.getBytes();
+        } else {
+            try {
+                final String name = CharacterSets.getMimeName(charset);
+                return data.getBytes(name);
+            } catch (final UnsupportedEncodingException e) {
+                return data.getBytes();
+            }
+        }
+    }
+
+    private static final Uri THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID");
+    // Copied from frameworks/opt/telephony/src/java/android/provider/Telephony.java because we
+    // can't use ContentResolver during backup/restore.
+    private static long getOrCreateThreadId(
+            ContentProvider contentProvider, Set<String> recipients) {
+        Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
+
+        for (String recipient : recipients) {
+            if (Telephony.Mms.isEmailAddress(recipient)) {
+                recipient = Telephony.Mms.extractAddrSpec(recipient);
+            }
+
+            uriBuilder.appendQueryParameter("recipient", recipient);
+        }
+
+        Uri uri = uriBuilder.build();
+
+        try (Cursor cursor = contentProvider.query(uri, PROJECTION_ID, null, null, null)) {
+            if (cursor != null) {
+                if (cursor.moveToFirst()) {
+                    return cursor.getLong(0);
+                } else {
+                    Log.e(TAG, "getOrCreateThreadId returned no rows!");
+                }
+            }
+        }
+
+        Log.e(TAG, "getOrCreateThreadId failed with " + recipients.size() + " recipients");
+        throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
+    }
+    @Override
+    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+                         ParcelFileDescriptor newState) throws IOException {
+        // Empty because is not used during full backup.
+    }
+
+    @Override
+    public void onRestore(BackupDataInput data, int appVersionCode,
+                          ParcelFileDescriptor newState) throws IOException {
+        // Empty because is not used during full restore.
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..1041716
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,19 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := mockito-target
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+LOCAL_PACKAGE_NAME := TelephonyProviderTests
+LOCAL_CERTIFICATE := platform
+
+LOCAL_INSTRUMENTATION_FOR := TelephonyProvider
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..7a273fc
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.providers.telephony.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.providers.telephony"
+        android:label="Tests for TelephonyProvider">
+    </instrumentation>
+</manifest>
diff --git a/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java b/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
new file mode 100644
index 0000000..5ce7a47
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2016 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.providers.telephony;
+
+import android.annotation.TargetApi;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.BaseColumns;
+import android.provider.Telephony;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockCursor;
+import android.util.ArrayMap;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.SparseArray;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Tests for testing backup/restore of SMS and text MMS messages.
+ * For backup it creates fake provider and checks resulting json array.
+ * For restore provides json array and checks inserts of the messages into provider.
+ */
+@TargetApi(Build.VERSION_CODES.M)
+public class TelephonyBackupAgentTest extends AndroidTestCase {
+    private SparseArray<String> mSubId2Phone;
+    private ArrayMap<String, Integer> mPhone2SubId;
+    private final List<ContentValues> mSmsTable = new ArrayList<>();
+    private final List<ContentValues> mMmsTable = new ArrayList<>();
+    private final List<ContentValues> mMmsAllContentValues = new ArrayList<>();
+    private FakeCursor mSmsCursor, mMmsCursor;
+    private ContentValues[] mSmsRows, mMmsRows;
+    private ContentValues mMmsNonText;
+    private String[] mSmsJson, mMmsJson;
+    private String mAllSmsJson, mAllMmsJson;
+
+
+    private StringWriter mStringWriter;
+    private Map<Uri, FakeCursor> mCursors;
+    private MockContentProvider mContentProvider;
+
+    private static final String EMPTY_JSON_ARRAY = "[]";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mStringWriter = new StringWriter();
+        mSubId2Phone = new SparseArray<String>();
+        mSubId2Phone.append(1, "+111111111111111");
+        mSubId2Phone.append(3, "+333333333333333");
+
+        mPhone2SubId = new ArrayMap<>();
+        for (int i=0; i<mSubId2Phone.size(); ++i) {
+            mPhone2SubId.put(mSubId2Phone.valueAt(i), mSubId2Phone.keyAt(i));
+        }
+
+        mSmsCursor = new FakeCursor(mSmsTable, TelephonyBackupAgent.SMS_PROJECTION);
+        mMmsCursor = new FakeCursor(mMmsTable, TelephonyBackupAgent.MMS_PROJECTION);
+        mCursors = new HashMap<Uri, FakeCursor>();
+
+        mSmsRows = new ContentValues[3];
+        mSmsJson = new String[3];
+        mSmsRows[0] = createSmsRow(1, 1, "+1232132214124", "sms 1", "sms subject", 9087978987l,
+                999999999, 3, 44, 1);
+        mSmsJson[0] = "{\"self_phone\":\"+111111111111111\",\"address\":" +
+                "\"+1232132214124\",\"body\":\"sms 1\",\"subject\":\"sms subject\",\"date\":" +
+                "\"9087978987\",\"date_sent\":\"999999999\",\"status\":\"3\",\"type\":\"44\"}";
+
+        mSmsRows[1] = createSmsRow(2, 2, "+1232132214124", "sms 2", null, 9087978987l, 999999999,
+                0, 4, 1);
+        mSmsJson[1] = "{\"address\":\"+1232132214124\",\"body\":\"sms 2\",\"date\":" +
+                "\"9087978987\",\"date_sent\":\"999999999\",\"status\":\"0\",\"type\":\"4\"}";
+
+        mSmsRows[2] = createSmsRow(4, 3, "+1232221412433 +1232221412444", "sms 3", null,
+                111111111111l, 999999999, 2, 3, 2);
+        mSmsJson[2] =  "{\"self_phone\":\"+333333333333333\",\"address\":" +
+                "\"+1232221412433 +1232221412444\",\"body\":\"sms 3\",\"date\":\"111111111111\"," +
+                "\"date_sent\":" +
+                "\"999999999\",\"status\":\"2\",\"type\":\"3\"}";
+
+        mAllSmsJson = concatJson(mSmsJson);
+
+
+
+        mMmsRows = new ContentValues[3];
+        mMmsJson = new String[3];
+        mMmsRows[0] = createMmsRow(1 /*id*/, 1 /*subid*/, "Subject 1" /*subject*/,
+                100 /*subcharset*/, 111111 /*date*/, 111112 /*datesent*/, 3 /*type*/,
+                17 /*version*/, 1 /*textonly*/,
+                11 /*msgBox*/, "location 1" /*contentLocation*/, "MMs body 1" /*body*/,
+                111 /*body charset*/,
+                new String[]{"+11121212", "example@example.com", "+999999999"} /*addresses*/,
+                1 /*threadId*/);
+
+        mMmsJson[0] = "{\"self_phone\":\"+111111111111111\",\"sub\":\"Subject 1\"," +
+                "\"date\":\"111111\",\"date_sent\":\"111112\",\"m_type\":\"3\",\"v\":\"17\"," +
+                "\"msg_box\":\"11\",\"ct_l\":\"location 1\",\"mms_addresses\":[{\"type\":10," +
+                "\"address\":\"+11121212\",\"charset\":100},{\"type\":11,\"address\":" +
+                "\"example@example.com\",\"charset\":101},{\"type\":12,\"address\":\"+999999999\"" +
+                ",\"charset\":102}],\"mms_body\":\"MMs body 1\",\"mms_charset\":111,\"" +
+                "sub_cs\":\"100\"}";
+
+        mMmsRows[1] = createMmsRow(2 /*id*/, 2 /*subid*/, null /*subject*/, 100 /*subcharset*/,
+                111122 /*date*/, 1111112 /*datesent*/, 4 /*type*/, 18 /*version*/, 1 /*textonly*/,
+                222 /*msgBox*/, "location 2" /*contentLocation*/, "MMs body 2" /*body*/,
+                121 /*body charset*/,
+                new String[]{"example@example.com", "+999999999"} /*addresses*/, 2 /*threadId*/);
+
+        mMmsJson[1] = "{\"date\":\"111122\",\"date_sent\":\"1111112\",\"m_type\":\"4\"," +
+                "\"v\":\"18\",\"msg_box\":\"222\",\"ct_l\":\"location 2\",\"mms_addresses\":" +
+                "[{\"type\":10,\"address\":\"example@example.com\",\"charset\":100}," +
+                "{\"type\":11,\"address\":\"+999999999\",\"charset\":101}]," +
+                "\"mms_body\":\"MMs body 2\",\"mms_charset\":121}";
+
+        mMmsRows[2] = createMmsRow(10 /*id*/, 3 /*subid*/, "Subject 10" /*subject*/,
+                10 /*subcharset*/, 111133 /*date*/, 1111132 /*datesent*/, 5 /*type*/,
+                19 /*version*/, 1 /*textonly*/,
+                333 /*msgBox*/, null /*contentLocation*/, "MMs body 3" /*body*/,
+                131 /*body charset*/, new String[]{"+8888888888"} /*addresses*/, 3 /*threadId*/);
+
+        mMmsJson[2] = "{\"self_phone\":\"+333333333333333\",\"sub\":\"Subject 10\"," +
+                "\"date\":\"111133\",\"date_sent\":\"1111132\",\"m_type\":\"5\",\"v\":\"19\"," +
+                "\"msg_box\":\"333\",\"mms_addresses\":[{\"type\":10,\"address\":\"+8888888888\"," +
+                "\"charset\":100}],\"mms_body\":\"MMs body 3\",\"mms_charset\":131," +
+                "\"sub_cs\":\"10\"}";
+        mAllMmsJson = concatJson(mMmsJson);
+
+
+        // Should not be backed up. Cause flag text_only is false.
+        mMmsNonText = createMmsRow(10 /*id*/, 3 /*subid*/, "Subject 10" /*subject*/,
+                10 /*subcharset*/,
+                111133 /*date*/, 1111132 /*datesent*/, 5 /*type*/, 19 /*version*/, 0 /*textonly*/,
+                333 /*msgBox*/, null /*contentLocation*/, "MMs body 3" /*body*/,
+                131 /*body charset*/, new String[]{"+8888888888"} /*addresses*/, 3 /*threadId*/);
+
+        mContentProvider = new MockContentProvider() {
+            @Override
+            public Cursor query(Uri uri, String[] projection, String selection,
+                                String[] selectionArgs, String sortOrder) {
+                if (mCursors.containsKey(uri)) {
+                    FakeCursor fakeCursor = mCursors.get(uri);
+                    if (projection != null) {
+                        fakeCursor.setProjection(projection);
+                    }
+                    return fakeCursor;
+                }
+                return super.query(uri, projection, selection, selectionArgs, sortOrder);
+            }
+        };
+
+    }
+
+    private static String concatJson(String[] json) {
+        StringBuilder stringBuilder = new StringBuilder("[");
+        for (int i=0; i<json.length; ++i) {
+            if (i > 0) {
+                stringBuilder.append(",");
+            }
+            stringBuilder.append(json[i]);
+        }
+        stringBuilder.append("]");
+        return stringBuilder.toString();
+    }
+
+    private static ContentValues createSmsRow(int id, int subId, String address, String body,
+                                              String subj, long date, long dateSent,
+                                              int status, int type, long threadId) {
+        ContentValues smsRow = new ContentValues();
+        smsRow.put(Telephony.Sms._ID, id);
+        smsRow.put(Telephony.Sms.SUBSCRIPTION_ID, subId);
+        if (address != null) {
+            smsRow.put(Telephony.Sms.ADDRESS, address);
+        }
+        if (body != null) {
+            smsRow.put(Telephony.Sms.BODY, body);
+        }
+        if (subj != null) {
+            smsRow.put(Telephony.Sms.SUBJECT, subj);
+        }
+        smsRow.put(Telephony.Sms.DATE, String.valueOf(date));
+        smsRow.put(Telephony.Sms.DATE_SENT, String.valueOf(dateSent));
+        smsRow.put(Telephony.Sms.STATUS, String.valueOf(status));
+        smsRow.put(Telephony.Sms.TYPE, String.valueOf(type));
+        smsRow.put(Telephony.Sms.THREAD_ID, threadId);
+
+        return smsRow;
+    }
+
+    private ContentValues createMmsRow(int id, int subId, String subj, int subCharset,
+                                       long date, long dateSent, int type, int version,
+                                       int textOnly, int msgBox,
+                                       String contentLocation, String body,
+                                       int bodyCharset, String[] addresses, long threadId) {
+        ContentValues mmsRow = new ContentValues();
+        mmsRow.put(Telephony.Mms._ID, id);
+        mmsRow.put(Telephony.Mms.SUBSCRIPTION_ID, subId);
+        if (subj != null) {
+            mmsRow.put(Telephony.Mms.SUBJECT, subj);
+            mmsRow.put(Telephony.Mms.SUBJECT_CHARSET, String.valueOf(subCharset));
+        }
+        mmsRow.put(Telephony.Mms.DATE, String.valueOf(date));
+        mmsRow.put(Telephony.Mms.DATE_SENT, String.valueOf(dateSent));
+        mmsRow.put(Telephony.Mms.MESSAGE_TYPE, String.valueOf(type));
+        mmsRow.put(Telephony.Mms.MMS_VERSION, String.valueOf(version));
+        mmsRow.put(Telephony.Mms.TEXT_ONLY, textOnly);
+        mmsRow.put(Telephony.Mms.MESSAGE_BOX, String.valueOf(msgBox));
+        if (contentLocation != null) {
+            mmsRow.put(Telephony.Mms.CONTENT_LOCATION, contentLocation);
+        }
+        mmsRow.put(Telephony.Mms.THREAD_ID, threadId);
+
+        final Uri partUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).
+                appendPath("part").build();
+        mCursors.put(partUri, createBodyCursor(body, bodyCharset));
+        mMmsAllContentValues.add(mmsRow);
+
+        final Uri addrUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).
+                appendPath("addr").build();
+        mCursors.put(addrUri, createAddrCursor(addresses));
+
+        return mmsRow;
+    }
+
+    private static final String APP_SMIL = "application/smil";
+    private static final String TEXT_PLAIN = "text/plain";
+
+    // Cursor with parts of Mms.
+    private FakeCursor createBodyCursor(String body, int charset) {
+        List<ContentValues> table = new ArrayList<>();
+
+        final String srcName = String.format("text.%06d.txt", 0);
+        final String smilBody = String.format(TelephonyBackupAgent.sSmilTextPart, srcName);
+        final String smil = String.format(TelephonyBackupAgent.sSmilTextOnly, smilBody);
+
+        final ContentValues smilPart = new ContentValues();
+        smilPart.put(Telephony.Mms.Part.SEQ, -1);
+        smilPart.put(Telephony.Mms.Part.CONTENT_TYPE, APP_SMIL);
+        smilPart.put(Telephony.Mms.Part.NAME, "smil.xml");
+        smilPart.put(Telephony.Mms.Part.CONTENT_ID, "<smil>");
+        smilPart.put(Telephony.Mms.Part.CONTENT_LOCATION, "smil.xml");
+        smilPart.put(Telephony.Mms.Part.TEXT, smil);
+        table.add(smilPart); // This part should not be backed up.
+        mMmsAllContentValues.add(smilPart);
+
+        final ContentValues bodyPart = new ContentValues();
+        bodyPart.put(Telephony.Mms.Part.SEQ, 0);
+        bodyPart.put(Telephony.Mms.Part.CONTENT_TYPE, TEXT_PLAIN);
+        bodyPart.put(Telephony.Mms.Part.NAME, srcName);
+        bodyPart.put(Telephony.Mms.Part.CONTENT_ID, "<"+srcName+">");
+        bodyPart.put(Telephony.Mms.Part.CONTENT_LOCATION, srcName);
+        bodyPart.put(Telephony.Mms.Part.CHARSET, charset);
+        bodyPart.put(Telephony.Mms.Part.TEXT, body);
+        table.add(bodyPart);
+        mMmsAllContentValues.add(bodyPart);
+
+        return new FakeCursor(table, TelephonyBackupAgent.MMS_TEXT_PROJECTION);
+    }
+
+    // Cursor with addresses of Mms.
+    private FakeCursor createAddrCursor(String[] addresses) {
+        List<ContentValues> table = new ArrayList<>();
+        for (int i=0; i<addresses.length; ++i) {
+            ContentValues addr = new ContentValues();
+            addr.put(Telephony.Mms.Addr.TYPE, 10+i);
+            addr.put(Telephony.Mms.Addr.ADDRESS, addresses[i]);
+            addr.put(Telephony.Mms.Addr.CHARSET, 100 + i);
+            mMmsAllContentValues.add(addr);
+            table.add(addr);
+        }
+        return new FakeCursor(table, TelephonyBackupAgent.MMS_ADDR_PROJECTION);
+    }
+
+    /**
+     * Test with no sms in the provider.
+     * @throws Exception
+     */
+    public void testBackupSms_NoSms() throws Exception {
+        TelephonyBackupAgent.putSmsMessagesToJson(mSmsCursor, mSubId2Phone,
+                new JsonWriter(mStringWriter), 1);
+        assertEquals(EMPTY_JSON_ARRAY, mStringWriter.toString());
+    }
+
+    /**
+     * Test with 3 sms in the provider with the limit per file 4.
+     * @throws Exception
+     */
+    public void testBackupSms_AllSms() throws Exception {
+        mSmsTable.addAll(Arrays.asList(mSmsRows));
+        TelephonyBackupAgent.putSmsMessagesToJson(mSmsCursor, mSubId2Phone,
+                new JsonWriter(mStringWriter), 4);
+        final String expected =
+                "[" + mSmsJson[0] + "," + mSmsJson[1] + "," + mSmsJson[2] + "]";
+        assertEquals(expected, mStringWriter.toString());
+    }
+
+    /**
+     * Test with 3 sms in the provider with the limit per file 3.
+     * @throws Exception
+     */
+    public void testBackupSms_AllSmsWithExactFileLimit() throws Exception {
+        mSmsTable.addAll(Arrays.asList(mSmsRows));
+        TelephonyBackupAgent.putSmsMessagesToJson(mSmsCursor, mSubId2Phone,
+                new JsonWriter(mStringWriter), 3);
+        final String expected =
+                "[" + mSmsJson[0] + "," + mSmsJson[1] + "," + mSmsJson[2] + "]";
+        assertEquals(expected, mStringWriter.toString());
+    }
+
+    /**
+     * Test with 3 sms in the provider with the limit per file 1.
+     * @throws Exception
+     */
+    public void testBackupSms_AllSmsOneMessagePerFile() throws Exception {
+        mSmsTable.addAll(Arrays.asList(mSmsRows));
+        TelephonyBackupAgent.putSmsMessagesToJson(mSmsCursor, mSubId2Phone,
+                new JsonWriter(mStringWriter), 1);
+        assertEquals("[" + mSmsJson[0] + "]", mStringWriter.toString());
+
+        mStringWriter = new StringWriter();
+        TelephonyBackupAgent.putSmsMessagesToJson(mSmsCursor, mSubId2Phone,
+                new JsonWriter(mStringWriter), 1);
+        assertEquals("[" + mSmsJson[1] + "]", mStringWriter.toString());
+
+        mStringWriter = new StringWriter();
+        TelephonyBackupAgent.putSmsMessagesToJson(mSmsCursor, mSubId2Phone,
+                new JsonWriter(mStringWriter), 1);
+        assertEquals("[" + mSmsJson[2] + "]", mStringWriter.toString());
+    }
+
+    /**
+     * Test with no mms in the pvovider.
+     * @throws Exception
+     */
+    public void testBackupMms_NoMms() throws Exception {
+        TelephonyBackupAgent.putMmsMessagesToJson(mMmsCursor, mContentProvider, mSubId2Phone,
+                new JsonWriter(mStringWriter), 4);
+        assertEquals(EMPTY_JSON_ARRAY, mStringWriter.toString());
+    }
+
+    /**
+     * Test with all mms.
+     * @throws Exception
+     */
+    public void testBackupMms_AllMms() throws Exception {
+        mMmsTable.addAll(Arrays.asList(mMmsRows));
+        mMmsTable.add(mMmsNonText);
+        TelephonyBackupAgent.putMmsMessagesToJson(mMmsCursor, mContentProvider, mSubId2Phone,
+                new JsonWriter(mStringWriter), 4);
+        final String expected =
+                "[" + mMmsJson[0] + "," + mMmsJson[1] + "," + mMmsJson[2] + "]";
+        assertEquals(expected, mStringWriter.toString());
+    }
+
+    /**
+     * Test with 3 mms in the provider with the limit per file 1.
+     * @throws Exception
+     */
+    public void testBackupMms_OneMessagePerFile() throws Exception {
+        mMmsTable.addAll(Arrays.asList(mMmsRows));
+        TelephonyBackupAgent.putMmsMessagesToJson(mMmsCursor, mContentProvider, mSubId2Phone,
+                new JsonWriter(mStringWriter), 1);
+        assertEquals("[" + mMmsJson[0] + "]", mStringWriter.toString());
+
+        mStringWriter = new StringWriter();
+        TelephonyBackupAgent.putMmsMessagesToJson(mMmsCursor, mContentProvider, mSubId2Phone,
+                new JsonWriter(mStringWriter), 1);
+        assertEquals("[" + mMmsJson[1] + "]", mStringWriter.toString());
+
+        mStringWriter = new StringWriter();
+        TelephonyBackupAgent.putMmsMessagesToJson(mMmsCursor, mContentProvider, mSubId2Phone,
+                new JsonWriter(mStringWriter), 2);
+        assertEquals("[" + mMmsJson[2] + "]", mStringWriter.toString());
+    }
+
+    /**
+     * Test with 3 mms in the provider with the limit per file 3.
+     * @throws Exception
+     */
+    public void testBackupMms_WithExactFileLimit() throws Exception {
+        mMmsTable.addAll(Arrays.asList(mMmsRows));
+        TelephonyBackupAgent.putMmsMessagesToJson(mMmsCursor, mContentProvider, mSubId2Phone,
+                new JsonWriter(mStringWriter), 3);
+        final String expected =
+                "[" + mMmsJson[0] + "," + mMmsJson[1] + "," + mMmsJson[2] + "]";
+        assertEquals(expected, mStringWriter.toString());
+    }
+
+    /**
+     * Test restore sms with the empty json array "[]".
+     * @throws Exception
+     */
+    public void testRestoreSms_NoSms() throws Exception {
+        JsonReader jsonReader = new JsonReader(new StringReader(EMPTY_JSON_ARRAY));
+        FakeSmsProvider smsProvider = new FakeSmsProvider(null);
+        TelephonyBackupAgent.putSmsMessagesToProvider(jsonReader, smsProvider,
+                new ThreadProvider(), mPhone2SubId);
+        assertEquals(0, smsProvider.getRowsAdded());
+    }
+
+    /**
+     * Test restore sms with three sms json object in the array.
+     * @throws Exception
+     */
+    public void testRestoreSms_AllSms() throws Exception {
+        JsonReader jsonReader = new JsonReader(new StringReader(mAllSmsJson));
+        FakeSmsProvider smsProvider = new FakeSmsProvider(mSmsRows);
+        TelephonyBackupAgent.putSmsMessagesToProvider(jsonReader, smsProvider,
+                new ThreadProvider(), mPhone2SubId);
+        assertEquals(mSmsRows.length, smsProvider.getRowsAdded());
+    }
+
+    /**
+     * Test restore mms with the empty json array "[]".
+     * @throws Exception
+     */
+    public void testRestoreMms_NoMms() throws Exception {
+        JsonReader jsonReader = new JsonReader(new StringReader(EMPTY_JSON_ARRAY));
+        FakeMmsProvider mmsProvider = new FakeMmsProvider(null);
+        TelephonyBackupAgent.putMmsMessagesToProvider(jsonReader, mmsProvider,
+                new ThreadProvider(), mPhone2SubId);
+        assertEquals(0, mmsProvider.getRowsAdded());
+    }
+
+    /**
+     * Test restore sms with three mms json object in the array.
+     * @throws Exception
+     */
+    public void testRestoreMms_AllMms() throws Exception {
+        JsonReader jsonReader = new JsonReader(new StringReader(mAllMmsJson));
+        FakeMmsProvider mmsProvider = new FakeMmsProvider(mMmsAllContentValues);
+        TelephonyBackupAgent.putMmsMessagesToProvider(jsonReader, mmsProvider,
+                new ThreadProvider(), mPhone2SubId);
+        assertEquals(15, mmsProvider.getRowsAdded());
+    }
+
+    /**
+     * class for checking sms insertion into the provider on restore.
+     */
+    private class FakeSmsProvider extends MockContentProvider {
+        private int nextRow = 0;
+        private ContentValues[] mSms;
+
+        public FakeSmsProvider(ContentValues[] sms) {
+            this.mSms = sms;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            assertEquals(Telephony.Sms.CONTENT_URI, uri);
+            ContentValues modifiedValues = new ContentValues(mSms[nextRow++]);
+            modifiedValues.remove(Telephony.Sms._ID);
+            modifiedValues.put(Telephony.Sms.READ, 1);
+            modifiedValues.put(Telephony.Sms.SEEN, 1);
+            if (mSubId2Phone.get(modifiedValues.getAsInteger(Telephony.Sms.SUBSCRIPTION_ID))
+                    == null) {
+                modifiedValues.put(Telephony.Sms.SUBSCRIPTION_ID, -1);
+            }
+
+            assertEquals(modifiedValues, values);
+            return null;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                            String sortOrder) {
+            return null;
+        }
+
+        public int getRowsAdded() {
+            return nextRow;
+        }
+    }
+
+    /**
+     * class for checking mms insertion into the provider on restore.
+     */
+    private class FakeMmsProvider extends MockContentProvider {
+        private int nextRow = 0;
+        private List<ContentValues> mValues;
+        private long mDummyMsgId = -1;
+        private long mMsgId = -1;
+
+        public FakeMmsProvider(List<ContentValues> values) {
+            this.mValues = values;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            Uri retUri = Uri.parse("dummy_uri");
+            ContentValues modifiedValues = new ContentValues(mValues.get(nextRow++));
+            if (APP_SMIL.equals(values.get(Telephony.Mms.Part.CONTENT_TYPE))) {
+                // Smil part.
+                assertEquals(-1, mDummyMsgId);
+                mDummyMsgId = values.getAsLong(Telephony.Mms.Part.MSG_ID);
+            }
+
+            if (values.get(Telephony.Mms.Part.SEQ) != null) {
+                // Part of mms.
+                final Uri expectedUri = Telephony.Mms.CONTENT_URI.buildUpon()
+                        .appendPath(String.valueOf(mDummyMsgId))
+                        .appendPath("part")
+                        .build();
+                assertEquals(expectedUri, uri);
+            }
+
+            if (values.get(Telephony.Mms.Part.MSG_ID) != null) {
+                modifiedValues.put(Telephony.Mms.Part.MSG_ID, mDummyMsgId);
+            }
+
+
+            if (values.get(Telephony.Mms.SUBSCRIPTION_ID) != null) {
+                assertEquals(Telephony.Mms.CONTENT_URI, uri);
+                if (mSubId2Phone.get(modifiedValues.getAsInteger(Telephony.Sms.SUBSCRIPTION_ID))
+                        == null) {
+                    modifiedValues.put(Telephony.Sms.SUBSCRIPTION_ID, -1);
+                }
+                // Mms.
+                modifiedValues.put(Telephony.Mms.READ, 1);
+                modifiedValues.put(Telephony.Mms.SEEN, 1);
+                mMsgId = modifiedValues.getAsInteger(BaseColumns._ID);
+                retUri = Uri.withAppendedPath(Telephony.Mms.CONTENT_URI, String.valueOf(mMsgId));
+                modifiedValues.remove(BaseColumns._ID);
+            }
+
+            if (values.get(Telephony.Mms.Addr.ADDRESS) != null) {
+                // Address.
+                final Uri expectedUri = Telephony.Mms.CONTENT_URI.buildUpon()
+                        .appendPath(String.valueOf(mMsgId))
+                        .appendPath("addr")
+                        .build();
+                assertEquals(expectedUri, uri);
+                assertNotSame(-1, mMsgId);
+                modifiedValues.put(Telephony.Mms.Addr.MSG_ID, mMsgId);
+                mDummyMsgId = -1;
+            }
+
+            for (String key : modifiedValues.keySet()) {
+                assertEquals(modifiedValues.get(key), values.get(key));
+            }
+            assertEquals(modifiedValues.size(), values.size());
+            return retUri;
+        }
+
+        @Override
+        public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+            final Uri expectedUri = Telephony.Mms.CONTENT_URI.buildUpon()
+                    .appendPath(String.valueOf(mDummyMsgId))
+                    .appendPath("part")
+                    .build();
+            assertEquals(expectedUri, uri);
+            ContentValues expected = new ContentValues();
+            expected.put(Telephony.Mms.Part.MSG_ID, mMsgId);
+            assertEquals(expected, values);
+            return 2;
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                            String sortOrder) {
+            return null;
+        }
+
+        public int getRowsAdded() {
+            return nextRow;
+        }
+    }
+
+    /**
+     * class that implements MmsSms provider for thread ids.
+     */
+    private static class ThreadProvider extends MockContentProvider {
+
+        Map<List<String>, Integer> threadIds;
+
+        public ThreadProvider() {
+            threadIds = new ArrayMap<>();
+        }
+
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                            String sortOrder) {
+            List<String> recipients = uri.getQueryParameters("recipient");
+            int threadId = threadIds.size() + 1;
+            if (threadIds.containsKey(recipients)) {
+                threadId = threadIds.get(recipients);
+            } else {
+                threadIds.put(recipients, threadId);
+            }
+
+            List<ContentValues> table = new ArrayList<>();
+            ContentValues row = new ContentValues();
+            row.put(BaseColumns._ID, String.valueOf(threadId));
+            table.add(row);
+            return new FakeCursor(table, projection);
+        }
+    }
+
+    /**
+     * general cursor for serving queries.
+     */
+    private static class FakeCursor extends MockCursor {
+        String[] projection;
+        List<ContentValues> rows;
+        int nextRow = -1;
+
+        public FakeCursor(List<ContentValues> rows, String[] projection) {
+            this.projection = projection;
+            this.rows = rows;
+        }
+
+        public void setProjection(String[] projection) {
+            this.projection = projection;
+        }
+
+        @Override
+        public int getColumnCount() {
+            return projection.length;
+        }
+
+        @Override
+        public String getColumnName(int columnIndex) {
+            return projection[columnIndex];
+        }
+
+        @Override
+        public String getString(int columnIndex) {
+            return rows.get(nextRow).getAsString(projection[columnIndex]);
+        }
+
+        @Override
+        public int getInt(int columnIndex) {
+            return rows.get(nextRow).getAsInteger(projection[columnIndex]);
+        }
+
+        @Override
+        public long getLong(int columnIndex) {
+            return rows.get(nextRow).getAsLong(projection[columnIndex]);
+        }
+
+        @Override
+        public boolean isAfterLast() {
+            return nextRow >= getCount();
+        }
+
+        @Override
+        public boolean isLast() {
+            return nextRow == getCount() - 1;
+        }
+
+        @Override
+        public boolean moveToFirst() {
+            nextRow = 0;
+            return getCount() > 0;
+        }
+
+        @Override
+        public boolean moveToNext() {
+            return getCount() > ++nextRow;
+        }
+
+        @Override
+        public int getCount() {
+            return rows.size();
+        }
+
+        @Override
+        public int getColumnIndex(String columnName) {
+            for (int i=0; i<projection.length; ++i) {
+                if (columnName.equals(projection[i])) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        @Override
+        public void close() {
+        }
+    }
+}