Merge "Add support for deleting APNs." am: 80178f441a am: 16982e4f79
am: 35c90eb26e

Change-Id: I5d63cc093f4857131e552b955cfa3bbd6aa980da
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 1ca6c9a..5ac7fab 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -91,6 +91,13 @@
                   android:singleUser="true"
                   android:readPermission="android.permission.READ_SMS" />
 
+        <provider android:name="CarrierProvider"
+                  android:authorities="carrier_information"
+                  android:exported="true"
+                  android:singleUser="true"
+                  android:multiprocess="false"
+                  android:writePermission="android.permission.MODIFY_PHONE_STATE" />
+
         <provider android:name="HbpcdLookupProvider"
                   android:authorities="hbpcd_lookup"
                   android:exported="true"
diff --git a/res/values/config.xml b/res/values/config.xml
index 6148e5e..23c08b8 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -7,4 +7,11 @@
         <item>310120</item>
         <item>311480</item>
     </string-array>
+
+    <!-- Specify a service to bind to that returns an implementation of the
+         IApnSourceService interface.
+         (e.g. com.foo/.Bar for the package com.foo and class com.foo.Bar)
+         If this value is empty or unparsable, we will apply APNs from the APN
+         conf xml file.  -->
+    <string name="apn_source_service" translatable="false"></string>
 </resources>
diff --git a/src/com/android/providers/telephony/CarrierDatabaseHelper.java b/src/com/android/providers/telephony/CarrierDatabaseHelper.java
new file mode 100644
index 0000000..5236b89
--- /dev/null
+++ b/src/com/android/providers/telephony/CarrierDatabaseHelper.java
@@ -0,0 +1,85 @@
+/*
+**
+** Copyright (C) 2014, 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.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+public class CarrierDatabaseHelper extends SQLiteOpenHelper {
+    private static final String TAG = "CarrierDatabaseHelper";
+    private static final boolean DBG = true;
+
+    private static final String DATABASE_NAME = "CarrierInformation.db";
+    public static final String CARRIER_KEY_TABLE = "carrier_key";
+    private static final int DATABASE_VERSION = 1;
+
+    /**
+     * CarrierDatabaseHelper carrier database helper class.
+     * @param context of the user.
+     */
+    public CarrierDatabaseHelper(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+    }
+
+    static final String KEY_TYPE = "key_type";
+    static final String KEY = "key";
+    static final String MCC = "mcc";
+    static final String MNC = "mnc";
+    static final String MVNO_TYPE = "mvno_type";
+    static final String MVNO_MATCH_DATA = "mvno_match_data";
+    static final String PUBLIC_CERTIFICATE = "public_certificate";
+    static final String LAST_MODIFIED = "last_modified";
+
+    private static final List<String> CARRIERS_UNIQUE_FIELDS = new ArrayList<String>();
+
+    static {
+        CARRIERS_UNIQUE_FIELDS.add(MCC);
+        CARRIERS_UNIQUE_FIELDS.add(MNC);
+        CARRIERS_UNIQUE_FIELDS.add(KEY_TYPE);
+        CARRIERS_UNIQUE_FIELDS.add(MVNO_TYPE);
+        CARRIERS_UNIQUE_FIELDS.add(MVNO_MATCH_DATA);
+    }
+
+    public static String getStringForCarrierKeyTableCreation(String tableName) {
+        return "CREATE TABLE " + tableName +
+                "(_id INTEGER PRIMARY KEY," +
+                MCC + " TEXT DEFAULT ''," +
+                MNC + " TEXT DEFAULT ''," +
+                MVNO_TYPE + " TEXT DEFAULT ''," +
+                MVNO_MATCH_DATA + " TEXT DEFAULT ''," +
+                KEY_TYPE + " TEXT DEFAULT ''," +
+                KEY + " TEXT DEFAULT ''," +
+                PUBLIC_CERTIFICATE + " TEXT DEFAULT ''," +
+                LAST_MODIFIED + " INTEGER DEFAULT 0," +
+                "UNIQUE (" + TextUtils.join(", ", CARRIERS_UNIQUE_FIELDS) + "));";
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(getStringForCarrierKeyTableCreation(CARRIER_KEY_TABLE));
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        // do nothing
+    }
+}
diff --git a/src/com/android/providers/telephony/CarrierProvider.java b/src/com/android/providers/telephony/CarrierProvider.java
new file mode 100644
index 0000000..1c85806
--- /dev/null
+++ b/src/com/android/providers/telephony/CarrierProvider.java
@@ -0,0 +1,122 @@
+/*
+ * 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.providers.telephony;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.util.Log;
+
+import android.content.ContentUris;
+import android.database.SQLException;
+
+import java.util.Arrays;
+
+/**
+ * The class to provide base facility to access Carrier related content,
+ * which is stored in a SQLite database.
+ */
+public class CarrierProvider extends ContentProvider {
+
+    private static final boolean VDBG = false; // STOPSHIP if true
+    private static final String TAG = "CarrierProvider";
+
+    private CarrierDatabaseHelper mDbHelper;
+    private SQLiteDatabase mDatabase;
+
+    static final String PROVIDER_NAME = "carrier_information";
+    static final String URL = "content://" + PROVIDER_NAME + "/carrier";
+    static final Uri CONTENT_URI = Uri.parse(URL);
+
+    @Override
+    public boolean onCreate() {
+        Log.d(TAG, "onCreate");
+        mDbHelper = new CarrierDatabaseHelper(getContext());
+        return (mDatabase == null ? false : true);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projectionIn, String selection,
+                        String[] selectionArgs, String sortOrder) {
+        if (VDBG) {
+            Log.d(TAG, "query:"
+                    + " uri=" + uri
+                    + " values=" + Arrays.toString(projectionIn)
+                    + " selection=" + selection
+                    + " selectionArgs=" + Arrays.toString(selectionArgs));
+        }
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(CarrierDatabaseHelper.CARRIER_KEY_TABLE);
+
+        SQLiteDatabase db = getReadableDatabase();
+        Cursor c = qb.query(db, projectionIn, selection, selectionArgs, null, null, sortOrder);
+        return c;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        values.put(CarrierDatabaseHelper.LAST_MODIFIED, System.currentTimeMillis());
+        long row = getWritableDatabase().insert(CarrierDatabaseHelper.CARRIER_KEY_TABLE,
+                null, values);
+        if (row > 0) {
+            Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
+            getContext().getContentResolver().notifyChange(newUri, null);
+            return newUri;
+        }
+        throw new SQLException("Fail to add a new record into " + uri);
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException("Cannot delete URL: " + uri);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+
+        if (VDBG) {
+            Log.d(TAG, "update:"
+                    + " uri=" + uri
+                    + " values={" + values + "}"
+                    + " selection=" + selection
+                    + " selectionArgs=" + Arrays.toString(selectionArgs));
+        }
+        final int count = getWritableDatabase().update(CarrierDatabaseHelper.CARRIER_KEY_TABLE,
+                values, selection, selectionArgs);
+        Log.d(TAG, "  update.count=" + count);
+        return count;
+    }
+
+    /**
+     * These methods can be overridden in a subclass for testing TelephonyProvider using an
+     * in-memory database.
+     */
+    SQLiteDatabase getReadableDatabase() {
+        return mDbHelper.getReadableDatabase();
+    }
+    SQLiteDatabase getWritableDatabase() {
+        return mDbHelper.getWritableDatabase();
+    }
+}
diff --git a/src/com/android/providers/telephony/MmsProvider.java b/src/com/android/providers/telephony/MmsProvider.java
index f588b23..547b22e 100644
--- a/src/com/android/providers/telephony/MmsProvider.java
+++ b/src/com/android/providers/telephony/MmsProvider.java
@@ -302,14 +302,6 @@
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
-        // The _data column is filled internally in MmsProvider, so this check is just to avoid
-        // it from being inadvertently set. This is not supposed to be a protection against
-        // malicious attack, since sql injection could still be attempted to bypass the check. On
-        // the other hand, the MmsProvider does verify that the _data column has an allowed value
-        // before opening any uri/files.
-        if (values != null && values.containsKey(Part._DATA)) {
-            return null;
-        }
         final int callerUid = Binder.getCallingUid();
         final String callerPkg = getCallingPackage();
         int msgBox = Mms.MESSAGE_BOX_ALL;
@@ -429,6 +421,7 @@
 
             res = Uri.parse(res + "/addr/" + rowId);
         } else if (table.equals(TABLE_PART)) {
+            boolean containsDataPath = values != null && values.containsKey(Part._DATA);
             finalValues = new ContentValues(values);
 
             if (match == MMS_MSG_PART) {
@@ -442,29 +435,67 @@
             boolean plainText = false;
             boolean smilText = false;
             if ("text/plain".equals(contentType)) {
+                if (containsDataPath) {
+                    Log.e(TAG, "insert: can't insert text/plain with _data");
+                    return null;
+                }
                 plainText = true;
             } else if ("application/smil".equals(contentType)) {
+                if (containsDataPath) {
+                    Log.e(TAG, "insert: can't insert application/smil with _data");
+                    return null;
+                }
                 smilText = true;
             }
             if (!plainText && !smilText) {
-                // Use the filename if possible, otherwise use the current time as the name.
-                String contentLocation = values.getAsString("cl");
-                if (!TextUtils.isEmpty(contentLocation)) {
-                    File f = new File(contentLocation);
-                    contentLocation = "_" + f.getName();
+                String path;
+                if (containsDataPath) {
+                    // The _data column is filled internally in MmsProvider or from the
+                    // TelephonyBackupAgent, so this check is just to avoid it from being
+                    // inadvertently set. This is not supposed to be a protection against malicious
+                    // attack, since sql injection could still be attempted to bypass the check.
+                    // On the other hand, the MmsProvider does verify that the _data column has an
+                    // allowed value before opening any uri/files.
+                    if (!"com.android.providers.telephony".equals(callerPkg)) {
+                        Log.e(TAG, "insert: can't insert _data");
+                        return null;
+                    }
+                    try {
+                        path = values.getAsString(Part._DATA);
+                        final String partsDirPath = getContext()
+                                .getDir(PARTS_DIR_NAME, 0).getCanonicalPath();
+                        if (!new File(path).getCanonicalPath().startsWith(partsDirPath)) {
+                            Log.e(TAG, "insert: path "
+                                    + path
+                                    + " does not start with "
+                                    + partsDirPath);
+                            // Don't care return value
+                            return null;
+                        }
+                    } catch (IOException e) {
+                        Log.e(TAG, "insert part: create path failed " + e, e);
+                        return null;
+                    }
                 } else {
-                    contentLocation = "";
-                }
+                    // Use the filename if possible, otherwise use the current time as the name.
+                    String contentLocation = values.getAsString("cl");
+                    if (!TextUtils.isEmpty(contentLocation)) {
+                        File f = new File(contentLocation);
+                        contentLocation = "_" + f.getName();
+                    } else {
+                        contentLocation = "";
+                    }
 
-                // Generate the '_data' field of the part with default
-                // permission settings.
-                String path = getContext().getDir(PARTS_DIR_NAME, 0).getPath()
-                        + "/PART_" + System.currentTimeMillis() + contentLocation;
+                    // Generate the '_data' field of the part with default
+                    // permission settings.
+                    path = getContext().getDir(PARTS_DIR_NAME, 0).getPath()
+                            + "/PART_" + System.currentTimeMillis() + contentLocation;
 
-                if (DownloadDrmHelper.isDrmConvertNeeded(contentType)) {
-                    // Adds the .fl extension to the filename if contentType is
-                    // "application/vnd.oma.drm.message"
-                    path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
+                    if (DownloadDrmHelper.isDrmConvertNeeded(contentType)) {
+                        // Adds the .fl extension to the filename if contentType is
+                        // "application/vnd.oma.drm.message"
+                        path = DownloadDrmHelper.modifyDrmFwLockFileExtension(path);
+                    }
                 }
 
                 finalValues.put(Part._DATA, path);
diff --git a/src/com/android/providers/telephony/MmsSmsProvider.java b/src/com/android/providers/telephony/MmsSmsProvider.java
index 1ea4d5c..1653cd9 100644
--- a/src/com/android/providers/telephony/MmsSmsProvider.java
+++ b/src/com/android/providers/telephony/MmsSmsProvider.java
@@ -28,6 +28,7 @@
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.os.Binder;
+import android.os.Bundle;
 import android.os.UserHandle;
 import android.provider.BaseColumns;
 import android.provider.Telephony;
@@ -305,6 +306,9 @@
 
     private boolean mUseStrictPhoneNumberComparation;
 
+    private static final String METHOD_IS_RESTORING = "is_restoring";
+    private static final String IS_RESTORING_KEY = "restoring";
+
     @Override
     public boolean onCreate() {
         setAppOps(AppOpsManager.OP_READ_SMS, AppOpsManager.OP_WRITE_SMS);
@@ -1389,4 +1393,15 @@
         }
         writer.println("Default SMS app: " + defaultSmsApp);
     }
+
+    @Override
+    public Bundle call(String method, String arg, Bundle extras) {
+        if (METHOD_IS_RESTORING.equals(method)) {
+            Bundle result = new Bundle();
+            result.putBoolean(IS_RESTORING_KEY, TelephonyBackupAgent.getIsRestoring());
+            return result;
+        }
+        Log.w(LOG_TAG, "Ignored unsupported " + method + " call");
+        return null;
+    }
 }
diff --git a/src/com/android/providers/telephony/TelephonyBackupAgent.java b/src/com/android/providers/telephony/TelephonyBackupAgent.java
index 7a9d701..5f6eb10 100644
--- a/src/com/android/providers/telephony/TelephonyBackupAgent.java
+++ b/src/com/android/providers/telephony/TelephonyBackupAgent.java
@@ -91,18 +91,27 @@
  *  "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",
+ *  "attachments":[{"mime_type":"image/jpeg","filename":"image000000.jpg"}],
+ *  "smil":"<smil><head><layout><root-layout/><region id='Image' fit='meet' top='0' left='0'
+ *   height='100%' width='100%'/></layout></head><body><par dur='5000ms'><img src='image000000.jpg'
+ *   region='Image' /></par></body></smil>",
  *  "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.
  *
  *   It stores how many bytes we are over the quota and don't backup the oldest messages.
+ *
+ *   NOTE: presently, only MMS's with text are backed up. However, MMS's with attachments are
+ *   restored. In other words, this code can restore MMS attachments if the attachment data
+ *   is in the json, but it doesn't currently backup the attachment data in the json.
  */
 
 @TargetApi(Build.VERSION_CODES.M)
 public class TelephonyBackupAgent extends BackupAgent {
     private static final String TAG = "TelephonyBackupAgent";
     private static final boolean DEBUG = false;
+    private static volatile boolean sIsRestoring;
 
 
     // Copied from packages/apps/Messaging/src/com/android/messaging/sms/MmsUtils.java.
@@ -136,12 +145,20 @@
     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 list of attachments of MMS message.
+    private static final String MMS_ATTACHMENTS_KEY = "attachments";
+    // JSON key for SMIL part of the MMS.
+    private static final String MMS_SMIL_KEY = "smil";
     // JSON key for list of recipients of the message.
     private static final String RECIPIENTS = "recipients";
     // 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";
+    // JSON key for mime type.
+    private static final String MMS_MIME_TYPE = "mime_type";
+    // JSON key for attachment filename.
+    private static final String MMS_ATTACHMENT_FILENAME = "filename";
 
     // File names suffixes for backup/restore.
     private static final String SMS_BACKUP_FILE_SUFFIX = "_sms_backup";
@@ -165,6 +182,8 @@
     @VisibleForTesting
     static final String UNKNOWN_SENDER = "\u02BCUNKNOWN_SENDER!\u02BC";
 
+    private static String ATTACHMENT_DATA_PATH = "/app_parts/";
+
     // Thread id for UNKNOWN_SENDER.
     private long mUnknownSenderThreadId;
 
@@ -180,7 +199,8 @@
             Telephony.Sms.DATE_SENT,
             Telephony.Sms.STATUS,
             Telephony.Sms.TYPE,
-            Telephony.Sms.THREAD_ID
+            Telephony.Sms.THREAD_ID,
+            Telephony.Sms.READ
     };
 
     // Columns to fetch recepients of SMS.
@@ -203,7 +223,8 @@
             Telephony.Mms.MESSAGE_BOX,
             Telephony.Mms.CONTENT_LOCATION,
             Telephony.Mms.THREAD_ID,
-            Telephony.Mms.TRANSACTION_ID
+            Telephony.Mms.TRANSACTION_ID,
+            Telephony.Mms.READ
     };
 
     // Columns from addr database for backup/restore. This database is used for fetching addresses
@@ -242,6 +263,7 @@
     private static ContentValues sDefaultValuesSms = new ContentValues(5);
     private static ContentValues sDefaultValuesMms = new ContentValues(6);
     private static final ContentValues sDefaultValuesAddr = new ContentValues(2);
+    private static final ContentValues sDefaultValuesAttachments = new ContentValues(2);
 
     // Shared preferences for the backup agent.
     private static final String BACKUP_PREFS = "backup_shared_prefs";
@@ -256,7 +278,8 @@
 
 
     static {
-        // Consider restored messages read and seen.
+        // Consider restored messages read and seen by default. The actual data can override
+        // these values.
         sDefaultValuesSms.put(Telephony.Sms.READ, 1);
         sDefaultValuesSms.put(Telephony.Sms.SEEN, 1);
         sDefaultValuesSms.put(Telephony.Sms.ADDRESS, UNKNOWN_SENDER);
@@ -343,9 +366,8 @@
         try (
                 Cursor smsCursor = mContentResolver.query(Telephony.Sms.CONTENT_URI, SMS_PROJECTION,
                         null, null, ORDER_BY_DATE);
-                // Do not backup non text-only MMS's.
                 Cursor mmsCursor = mContentResolver.query(Telephony.Mms.CONTENT_URI, MMS_PROJECTION,
-                        Telephony.Mms.TEXT_ONLY+"=1", null, ORDER_BY_DATE)) {
+                        null, null, ORDER_BY_DATE)) {
 
             if (smsCursor != null) {
                 smsCursor.moveToFirst();
@@ -496,6 +518,8 @@
         protected void onHandleIntent(Intent intent) {
             try {
                 mWakeLock.acquire();
+                sIsRestoring = true;
+
                 File[] files = getFilesToRestore(this);
 
                 if (files == null || files.length == 0) {
@@ -503,18 +527,34 @@
                 }
                 Arrays.sort(files, mFileComparator);
 
+                boolean didRestore = false;
+
                 for (File file : files) {
                     final String fileName = file.getName();
+                    if (DEBUG) {
+                        Log.d(TAG, "onHandleIntent restoring file " + fileName);
+                    }
                     try (FileInputStream fileInputStream = new FileInputStream(file)) {
                         mTelephonyBackupAgent.doRestoreFile(fileName, fileInputStream.getFD());
+                        didRestore = true;
                     } catch (Exception e) {
                         // Either IOException or RuntimeException.
-                        Log.e(TAG, e.toString());
+                        Log.e(TAG, "onHandleIntent", e);
                     } finally {
                         file.delete();
                     }
                 }
-            } finally {
+                if (didRestore) {
+                  // Tell the default sms app to do a full sync now that the messages have been
+                  // restored.
+                  if (DEBUG) {
+                    Log.d(TAG, "onHandleIntent done - notifying default sms app");
+                  }
+                  ProviderUtil.notifyIfNotDefaultSmsApp(null /*uri*/, null /*calling package*/,
+                      this);
+                }
+           } finally {
+                sIsRestoring = false;
                 mWakeLock.release();
             }
         }
@@ -566,18 +606,18 @@
 
     private void doRestoreFile(String fileName, FileDescriptor fd) throws IOException {
         if (DEBUG) {
-            Log.i(TAG, "Restoring file " + fileName);
+            Log.d(TAG, "Restoring file " + fileName);
         }
 
         try (JsonReader jsonReader = getJsonReader(fd)) {
             if (fileName.endsWith(SMS_BACKUP_FILE_SUFFIX)) {
                 if (DEBUG) {
-                    Log.i(TAG, "Restoring SMS");
+                    Log.d(TAG, "Restoring SMS");
                 }
                 putSmsMessagesToProvider(jsonReader);
             } else if (fileName.endsWith(MMS_BACKUP_FILE_SUFFIX)) {
                 if (DEBUG) {
-                    Log.i(TAG, "Restoring text MMS");
+                    Log.d(TAG, "Restoring text MMS");
                 }
                 putMmsMessagesToProvider(jsonReader);
             } else {
@@ -616,6 +656,9 @@
         jsonReader.beginArray();
         while (jsonReader.hasNext()) {
             final Mms mms = readMmsFromReader(jsonReader);
+            if (DEBUG) {
+                Log.d(TAG, "putMmsMessagesToProvider " + mms);
+            }
             if (doesMmsExist(mms)) {
                 if (DEBUG) {
                     Log.e(TAG, String.format("Mms: %s already exists", mms.toString()));
@@ -761,6 +804,7 @@
                 case Telephony.Sms.TYPE:
                 case Telephony.Sms.SUBJECT:
                 case Telephony.Sms.ADDRESS:
+                case Telephony.Sms.READ:
                     values.put(name, jsonReader.nextString());
                     break;
                 case RECIPIENTS:
@@ -778,7 +822,7 @@
                     break;
                 default:
                     if (DEBUG) {
-                        Log.w(TAG, "Unknown name:" + name);
+                        Log.w(TAG, "readSmsValuesFromReader Unknown name:" + name);
                     }
                     jsonReader.skipValue();
                     break;
@@ -802,6 +846,7 @@
     private int writeMmsToWriter(JsonWriter jsonWriter, Cursor cursor) throws IOException {
         final int mmsId = cursor.getInt(ID_IDX);
         final MmsBody body = getMmsBody(mmsId);
+        // We backup any message that contains text, but only backup the text part.
         if (body == null || body.text == null) {
             return 0;
         }
@@ -811,6 +856,9 @@
         for (int i=0; i<cursor.getColumnCount(); ++i) {
             final String name = cursor.getColumnName(i);
             final String value = cursor.getString(i);
+            if (DEBUG) {
+                Log.d(TAG, "writeMmsToWriter name: " + name + " value: " + value);
+            }
             if (value == null) {
                 continue;
             }
@@ -862,6 +910,9 @@
         int bodyCharset = CharacterSets.DEFAULT_CHARSET;
         while (jsonReader.hasNext()) {
             String name = jsonReader.nextName();
+            if (DEBUG) {
+                Log.d(TAG, "readMmsFromReader " + name);
+            }
             switch (name) {
                 case SELF_PHONE_KEY:
                     final String selfPhone = jsonReader.nextString();
@@ -872,6 +923,12 @@
                 case MMS_ADDRESSES_KEY:
                     getMmsAddressesFromReader(jsonReader, mms);
                     break;
+                case MMS_ATTACHMENTS_KEY:
+                    getMmsAttachmentsFromReader(jsonReader, mms);
+                    break;
+                case MMS_SMIL_KEY:
+                    mms.smil = jsonReader.nextString();
+                    break;
                 case MMS_BODY_KEY:
                     bodyText = jsonReader.nextString();
                     break;
@@ -894,11 +951,12 @@
                 case Telephony.Mms.MESSAGE_BOX:
                 case Telephony.Mms.CONTENT_LOCATION:
                 case Telephony.Mms.TRANSACTION_ID:
+                case Telephony.Mms.READ:
                     mms.values.put(name, jsonReader.nextString());
                     break;
                 default:
                     if (DEBUG) {
-                        Log.w(TAG, "Unknown name:" + name);
+                        Log.d(TAG, "Unknown name:" + name);
                     }
                     jsonReader.skipValue();
                     break;
@@ -909,6 +967,9 @@
         if (bodyText != null) {
             mms.body = new MmsBody(bodyText, bodyCharset);
         }
+        // Set the text_only flag
+        mms.values.put(Telephony.Mms.TEXT_ONLY, (mms.attachments == null
+                || mms.attachments.size() == 0) && bodyText != null ? 1 : 0);
 
         // Set default charset for subject.
         if (mms.values.get(Telephony.Mms.SUBJECT) != null &&
@@ -952,9 +1013,11 @@
                 ORDER_BY_ID)) {
             if (cursor != null && cursor.moveToFirst()) {
                 do {
-                    body = (body == null ? cursor.getString(MMS_TEXT_IDX)
-                            : body.concat(cursor.getString(MMS_TEXT_IDX)));
-                    charSet = cursor.getInt(MMS_TEXT_CHARSET_IDX);
+                    String text = cursor.getString(MMS_TEXT_IDX);
+                    if (text != null) {
+                        body = (body == null ? text : body.concat(text));
+                        charSet = cursor.getInt(MMS_TEXT_CHARSET_IDX);
+                    }
                 } while (cursor.moveToNext());
             }
         }
@@ -1004,7 +1067,7 @@
                         break;
                     default:
                         if (DEBUG) {
-                            Log.w(TAG, "Unknown name:" + name);
+                            Log.d(TAG, "Unknown name:" + name);
                         }
                         jsonReader.skipValue();
                         break;
@@ -1018,9 +1081,46 @@
         jsonReader.endArray();
     }
 
+    private static void getMmsAttachmentsFromReader(JsonReader jsonReader, Mms mms)
+            throws IOException {
+        if (DEBUG) {
+            Log.d(TAG, "Add getMmsAttachmentsFromReader");
+        }
+        mms.attachments = new ArrayList<ContentValues>();
+        jsonReader.beginArray();
+        while (jsonReader.hasNext()) {
+            jsonReader.beginObject();
+            ContentValues attachmentValues = new ContentValues(sDefaultValuesAttachments);
+            while (jsonReader.hasNext()) {
+                final String name = jsonReader.nextName();
+                switch (name) {
+                    case MMS_MIME_TYPE:
+                    case MMS_ATTACHMENT_FILENAME:
+                        attachmentValues.put(name, jsonReader.nextString());
+                        break;
+                    default:
+                        if (DEBUG) {
+                            Log.d(TAG, "getMmsAttachmentsFromReader Unknown name:" + name);
+                        }
+                        jsonReader.skipValue();
+                        break;
+                }
+            }
+            jsonReader.endObject();
+            if (attachmentValues.containsKey(MMS_ATTACHMENT_FILENAME)) {
+                mms.attachments.add(attachmentValues);
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "Attachment json with no filenames");
+                }
+            }
+        }
+        jsonReader.endArray();
+    }
+
     private void addMmsMessage(Mms mms) {
         if (DEBUG) {
-            Log.e(TAG, "Add mms:\n" + mms.toString());
+            Log.d(TAG, "Add mms:\n" + mms);
         }
         final long dummyId = System.currentTimeMillis(); // Dummy ID of the msg.
         final Uri partUri = Telephony.Mms.CONTENT_URI.buildUpon()
@@ -1029,7 +1129,8 @@
         final String srcName = String.format(Locale.US, "text.%06d.txt", 0);
         { // Insert SMIL part.
             final String smilBody = String.format(sSmilTextPart, srcName);
-            final String smil = String.format(sSmilTextOnly, smilBody);
+            final String smil = TextUtils.isEmpty(mms.smil) ?
+                    String.format(sSmilTextOnly, smilBody) : mms.smil;
             final ContentValues values = new ContentValues(7);
             values.put(Telephony.Mms.Part.MSG_ID, dummyId);
             values.put(Telephony.Mms.Part.SEQ, -1);
@@ -1064,6 +1165,29 @@
             }
         }
 
+        if (mms.attachments != null) {
+            // Insert the attachment parts.
+            for (ContentValues mmsAttachment : mms.attachments) {
+                final ContentValues values = new ContentValues(6);
+                values.put(Telephony.Mms.Part.MSG_ID, dummyId);
+                values.put(Telephony.Mms.Part.SEQ, 0);
+                values.put(Telephony.Mms.Part.CONTENT_TYPE,
+                        mmsAttachment.getAsString(MMS_MIME_TYPE));
+                String filename = mmsAttachment.getAsString(MMS_ATTACHMENT_FILENAME);
+                values.put(Telephony.Mms.Part.CONTENT_ID, "<"+filename+">");
+                values.put(Telephony.Mms.Part.CONTENT_LOCATION, filename);
+                values.put(Telephony.Mms.Part._DATA,
+                        getDataDir() + ATTACHMENT_DATA_PATH + filename);
+                Uri newPartUri = mContentResolver.insert(partUri, values);
+                if (newPartUri == null) {
+                    if (DEBUG) {
+                        Log.e(TAG, "Could not insert attachment part");
+                    }
+                    return;
+                }
+            }
+        }
+
         // Insert mms.
         final Uri mmsUri = mContentResolver.insert(Telephony.Mms.CONTENT_URI, mms.values);
         if (mmsUri == null) {
@@ -1080,7 +1204,7 @@
             mContentResolver.update(partUri, values, null, null);
         }
 
-        { // Insert adderesses into "addr".
+        { // Insert addresses into "addr".
             final Uri addrUri = Uri.withAppendedPath(mmsUri, "addr");
             for (ContentValues mmsAddress : mms.addresses) {
                 ContentValues values = new ContentValues(mmsAddress);
@@ -1117,10 +1241,13 @@
     private static final class Mms {
         public ContentValues values;
         public List<ContentValues> addresses;
+        public List<ContentValues> attachments;
+        public String smil;
         public MmsBody body;
         @Override
         public String toString() {
-            return "Values:" + values.toString() + "\nRecipients:"+addresses.toString()
+            return "Values:" + values.toString() + "\nRecipients:" + addresses.toString()
+                    + "\nAttachments:" + (attachments == null ? "none" : attachments.toString())
                     + "\nBody:" + body;
         }
     }
@@ -1278,7 +1405,7 @@
                             numbers.add(number);
                         } else {
                             if (DEBUG) {
-                                Log.w(TAG, "Canonical MMS/SMS address is empty for id: " + longId);
+                                Log.d(TAG, "Canonical MMS/SMS address is empty for id: " + longId);
                             }
                         }
                     }
@@ -1289,7 +1416,7 @@
         }
         if (numbers.isEmpty()) {
             if (DEBUG) {
-                Log.w(TAG, "No MMS addresses found from ids string [" + spaceSepIds + "]");
+                Log.d(TAG, "No MMS addresses found from ids string [" + spaceSepIds + "]");
             }
         }
         return numbers;
@@ -1306,4 +1433,8 @@
                           ParcelFileDescriptor newState) throws IOException {
         // Empty because is not used during full restore.
     }
+
+    public static boolean getIsRestoring() {
+        return sIsRestoring;
+    }
 }
diff --git a/src/com/android/providers/telephony/TelephonyProvider.java b/src/com/android/providers/telephony/TelephonyProvider.java
index f622854..32e0b55 100644
--- a/src/com/android/providers/telephony/TelephonyProvider.java
+++ b/src/com/android/providers/telephony/TelephonyProvider.java
@@ -17,10 +17,56 @@
 
 package com.android.providers.telephony;
 
+import static android.provider.Telephony.Carriers.APN;
+import static android.provider.Telephony.Carriers.AUTH_TYPE;
+import static android.provider.Telephony.Carriers.BEARER;
+import static android.provider.Telephony.Carriers.BEARER_BITMASK;
+import static android.provider.Telephony.Carriers.CARRIER_DELETED;
+import static android.provider.Telephony.Carriers.CARRIER_DELETED_BUT_PRESENT_IN_XML;
+import static android.provider.Telephony.Carriers.CARRIER_EDITED;
+import static android.provider.Telephony.Carriers.CARRIER_ENABLED;
+import static android.provider.Telephony.Carriers.CONTENT_URI;
+import static android.provider.Telephony.Carriers.CURRENT;
+import static android.provider.Telephony.Carriers.EDITED;
+import static android.provider.Telephony.Carriers.MAX_CONNS;
+import static android.provider.Telephony.Carriers.MAX_CONNS_TIME;
+import static android.provider.Telephony.Carriers.MCC;
+import static android.provider.Telephony.Carriers.MMSC;
+import static android.provider.Telephony.Carriers.MMSPORT;
+import static android.provider.Telephony.Carriers.MMSPROXY;
+import static android.provider.Telephony.Carriers.MNC;
+import static android.provider.Telephony.Carriers.MODEM_COGNITIVE;
+import static android.provider.Telephony.Carriers.MTU;
+import static android.provider.Telephony.Carriers.MVNO_MATCH_DATA;
+import static android.provider.Telephony.Carriers.MVNO_TYPE;
+import static android.provider.Telephony.Carriers.NAME;
+import static android.provider.Telephony.Carriers.NUMERIC;
+import static android.provider.Telephony.Carriers.PASSWORD;
+import static android.provider.Telephony.Carriers.PORT;
+import static android.provider.Telephony.Carriers.PROFILE_ID;
+import static android.provider.Telephony.Carriers.PROTOCOL;
+import static android.provider.Telephony.Carriers.PROXY;
+import static android.provider.Telephony.Carriers.ROAMING_PROTOCOL;
+import static android.provider.Telephony.Carriers.SERVER;
+import static android.provider.Telephony.Carriers.SUBSCRIPTION_ID;
+import static android.provider.Telephony.Carriers.TYPE;
+import static android.provider.Telephony.Carriers.UNEDITED;
+import static android.provider.Telephony.Carriers.USER;
+import static android.provider.Telephony.Carriers.USER_DELETED;
+import static android.provider.Telephony.Carriers.USER_DELETED_BUT_PRESENT_IN_XML;
+import static android.provider.Telephony.Carriers.USER_EDITABLE;
+import static android.provider.Telephony.Carriers.USER_EDITED;
+import static android.provider.Telephony.Carriers.USER_VISIBLE;
+import static android.provider.Telephony.Carriers.WAIT_TIME;
+import static android.provider.Telephony.Carriers._ID;
+
+import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.SharedPreferences;
 import android.content.UriMatcher;
 import android.content.pm.PackageManager;
@@ -36,6 +82,8 @@
 import android.os.Binder;
 import android.os.Environment;
 import android.os.FileUtils;
+import android.os.IBinder;
+import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.telephony.ServiceState;
@@ -47,8 +95,10 @@
 import android.util.Pair;
 import android.util.Xml;
 
-import com.android.internal.util.XmlUtils;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.IApnSourceService;
+import com.android.internal.util.XmlUtils;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -62,15 +112,13 @@
 import java.util.List;
 import java.util.Map;
 
-import static android.provider.Telephony.Carriers.*;
-
 public class TelephonyProvider extends ContentProvider
 {
     private static final String DATABASE_NAME = "telephony.db";
     private static final boolean DBG = true;
     private static final boolean VDBG = false; // STOPSHIP if true
 
-    private static final int DATABASE_VERSION = 19 << 16;
+    private static final int DATABASE_VERSION = 21 << 16;
     private static final int URL_UNKNOWN = 0;
     private static final int URL_TELEPHONY = 1;
     private static final int URL_CURRENT = 2;
@@ -137,6 +185,12 @@
     private static final int INVALID_APN_ID = -1;
     private static final List<String> CARRIERS_UNIQUE_FIELDS = new ArrayList<String>();
 
+    private static Boolean s_apnSourceServiceExists;
+
+    protected final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private IApnSourceService mIApnSourceService;
+
     static {
         // Columns not included in UNIQUE constraint: name, current, edited, user, server, password,
         // authtype, type, protocol, roaming_protocol, sub_id, modem_cognitive, max_conns,
@@ -157,6 +211,7 @@
         CARRIERS_UNIQUE_FIELDS.add(PROFILE_ID);
         CARRIERS_UNIQUE_FIELDS.add(PROTOCOL);
         CARRIERS_UNIQUE_FIELDS.add(ROAMING_PROTOCOL);
+        CARRIERS_UNIQUE_FIELDS.add(USER_EDITABLE);
     }
 
     @VisibleForTesting
@@ -196,6 +251,7 @@
                 MTU + " INTEGER DEFAULT 0," +
                 EDITED + " INTEGER DEFAULT " + UNEDITED + "," +
                 USER_VISIBLE + " BOOLEAN DEFAULT 1," +
+                USER_EDITABLE + " BOOLEAN DEFAULT 1," +
                 // Uniqueness collisions are used to trigger merge code so if a field is listed
                 // here it means we will accept both (user edited + new apn_conf definition)
                 // Columns not included in UNIQUE constraint: name, current, edited,
@@ -225,6 +281,9 @@
             + SubscriptionManager.MNC + " INTEGER DEFAULT 0,"
             + SubscriptionManager.SIM_PROVISIONING_STATUS
                 + " INTEGER DEFAULT " + SubscriptionManager.SIM_PROVISIONED + ","
+            + SubscriptionManager.IS_EMBEDDED + " INTEGER DEFAULT 0,"
+            + SubscriptionManager.ACCESS_RULES + " BLOB,"
+            + SubscriptionManager.IS_REMOVABLE + " INTEGER DEFAULT 0,"
             + SubscriptionManager.CB_EXTREME_THREAT_ALERT + " INTEGER DEFAULT 1,"
             + SubscriptionManager.CB_SEVERE_THREAT_ALERT + " INTEGER DEFAULT 1,"
             + SubscriptionManager.CB_AMBER_ALERT + " INTEGER DEFAULT 1,"
@@ -305,7 +364,13 @@
             if (DBG) log("dbh.onCreate:+ db=" + db);
             createSimInfoTable(db);
             createCarriersTable(db, CARRIERS_TABLE);
-            initDatabase(db);
+            // if CarrierSettings app is installed, we expect it to do the initializiation instead
+            if (apnSourceServiceExists(mContext)) {
+                log("dbh.onCreate: Skipping apply APNs from xml.");
+            } else {
+                log("dbh.onCreate: Apply apns from xml.");
+                initDatabase(db);
+            }
             if (DBG) log("dbh.onCreate:- db=" + db);
         }
 
@@ -795,6 +860,38 @@
                 }
                 oldVersion = 19 << 16 | 6;
             }
+            if (oldVersion < (20 << 16 | 6)) {
+                try {
+                    // Try to update the siminfo table. It might not be there.
+                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN " +
+                            SubscriptionManager.IS_EMBEDDED + " INTEGER DEFAULT 0;");
+                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN " +
+                            SubscriptionManager.ACCESS_RULES + " BLOB;");
+                    db.execSQL("ALTER TABLE " + SIMINFO_TABLE + " ADD COLUMN " +
+                            SubscriptionManager.IS_REMOVABLE + " INTEGER DEFAULT 0;");
+                } catch (SQLiteException e) {
+                    if (DBG) {
+                        log("onUpgrade skipping " + SIMINFO_TABLE + " upgrade. " +
+                                "The table will get created in onOpen.");
+                    }
+                }
+                oldVersion = 20 << 16 | 6;
+            }
+            if (oldVersion < (21 << 16 | 6)) {
+                try {
+                    // Try to update the siminfo table. It might not be there.
+                    db.execSQL("ALTER TABLE " + CARRIERS_TABLE + " ADD COLUMN " +
+                            USER_EDITABLE + " INTEGER DEFAULT 1;");
+                } catch (SQLiteException e) {
+                    // This is possible if the column already exists which may be the case if the
+                    // table was just created as part of upgrade to version 19
+                    if (DBG) {
+                        log("onUpgrade skipping " + CARRIERS_TABLE + " upgrade. " +
+                                "The table will get created in onOpen.");
+                    }
+                }
+                oldVersion = 21 << 16 | 6;
+            }
             if (DBG) {
                 log("dbh.onUpgrade:- db=" + db + " oldV=" + oldVersion + " newV=" + newVersion);
             }
@@ -1208,6 +1305,7 @@
             addBoolAttribute(parser, "carrier_enabled", map, CARRIER_ENABLED);
             addBoolAttribute(parser, "modem_cognitive", map, MODEM_COGNITIVE);
             addBoolAttribute(parser, "user_visible", map, USER_VISIBLE);
+            addBoolAttribute(parser, "user_editable", map, USER_EDITABLE);
 
             int bearerBitmask = 0;
             String bearerList = parser.getAttributeValue(null, "bearer_bitmask");
@@ -1583,49 +1681,134 @@
         return mOpenHelper.apnDbUpdateNeeded();
     }
 
+    private static boolean apnSourceServiceExists(Context context) {
+        if (s_apnSourceServiceExists != null) {
+            return s_apnSourceServiceExists;
+        }
+        try {
+            String service = context.getResources().getString(R.string.apn_source_service);
+            if (TextUtils.isEmpty(service)) {
+                s_apnSourceServiceExists = false;
+            } else {
+                s_apnSourceServiceExists = context.getPackageManager().getServiceInfo(
+                        ComponentName.unflattenFromString(service), 0)
+                        != null;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            s_apnSourceServiceExists = false;
+        }
+        return s_apnSourceServiceExists;
+    }
+
+    private void restoreApnsWithService() {
+        Context context = getContext();
+        Resources r = context.getResources();
+        ServiceConnection connection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName className,
+                    IBinder service) {
+                log("restoreApnsWithService: onServiceConnected");
+                synchronized (mLock) {
+                    mIApnSourceService = IApnSourceService.Stub.asInterface(service);
+                    mLock.notifyAll();
+                }
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName arg0) {
+                loge("mIApnSourceService has disconnected unexpectedly");
+                synchronized (mLock) {
+                    mIApnSourceService = null;
+                }
+            }
+        };
+
+        Intent intent = new Intent(IApnSourceService.class.getName());
+        intent.setComponent(ComponentName.unflattenFromString(
+                r.getString(R.string.apn_source_service)));
+        log("binding to service to restore apns, intent=" + intent);
+        try {
+            if (context.bindService(intent, connection, Context.BIND_AUTO_CREATE)) {
+                synchronized (mLock) {
+                    while (mIApnSourceService == null) {
+                        try {
+                            mLock.wait();
+                        } catch (InterruptedException e) {
+                            loge("Error while waiting for service connection: " + e);
+                        }
+                    }
+                    try {
+                        ContentValues[] values = mIApnSourceService.getApns();
+                        if (values != null) {
+                            // we use the unsynchronized insert because this function is called
+                            // within the syncrhonized function delete()
+                            unsynchronizedBulkInsert(CONTENT_URI, values);
+                            log("restoreApnsWithService: restored");
+                        }
+                    } catch (RemoteException e) {
+                        loge("Error applying apns from service: " + e);
+                    }
+                }
+            } else {
+                loge("unable to bind to service from intent=" + intent);
+            }
+        } catch (SecurityException e) {
+            loge("Error applying apns from service: " + e);
+        } finally {
+            if (connection != null) {
+                context.unbindService(connection);
+            }
+            synchronized (mLock) {
+                mIApnSourceService = null;
+            }
+        }
+    }
+
 
     @Override
     public boolean onCreate() {
         mOpenHelper = new DatabaseHelper(getContext());
 
-        // Call getReadableDatabase() to make sure onUpgrade is called
-        if (VDBG) log("onCreate: calling getReadableDatabase to trigger onUpgrade");
-        SQLiteDatabase db = getReadableDatabase();
+        if (!apnSourceServiceExists(getContext())) {
+            // Call getReadableDatabase() to make sure onUpgrade is called
+            if (VDBG) log("onCreate: calling getReadableDatabase to trigger onUpgrade");
+            SQLiteDatabase db = getReadableDatabase();
 
-        // Update APN db on build update
-        String newBuildId = SystemProperties.get("ro.build.id", null);
-        if (!TextUtils.isEmpty(newBuildId)) {
-            // Check if build id has changed
-            SharedPreferences sp = getContext().getSharedPreferences(BUILD_ID_FILE,
-                    Context.MODE_PRIVATE);
-            String oldBuildId = sp.getString(RO_BUILD_ID, "");
-            if (!newBuildId.equals(oldBuildId)) {
-                if (DBG) log("onCreate: build id changed from " + oldBuildId + " to " +
-                        newBuildId);
+            // Update APN db on build update
+            String newBuildId = SystemProperties.get("ro.build.id", null);
+            if (!TextUtils.isEmpty(newBuildId)) {
+                // Check if build id has changed
+                SharedPreferences sp = getContext().getSharedPreferences(BUILD_ID_FILE,
+                        Context.MODE_PRIVATE);
+                String oldBuildId = sp.getString(RO_BUILD_ID, "");
+                if (!newBuildId.equals(oldBuildId)) {
+                    if (DBG) log("onCreate: build id changed from " + oldBuildId + " to " +
+                            newBuildId);
 
-                // Get rid of old preferred apn shared preferences
-                SubscriptionManager sm = SubscriptionManager.from(getContext());
-                if (sm != null) {
-                    List<SubscriptionInfo> subInfoList = sm.getAllSubscriptionInfoList();
-                    for (SubscriptionInfo subInfo : subInfoList) {
-                        SharedPreferences spPrefFile = getContext().getSharedPreferences(
-                                PREF_FILE_APN + subInfo.getSubscriptionId(), Context.MODE_PRIVATE);
-                        if (spPrefFile != null) {
-                            SharedPreferences.Editor editor = spPrefFile.edit();
-                            editor.clear();
-                            editor.apply();
+                    // Get rid of old preferred apn shared preferences
+                    SubscriptionManager sm = SubscriptionManager.from(getContext());
+                    if (sm != null) {
+                        List<SubscriptionInfo> subInfoList = sm.getAllSubscriptionInfoList();
+                        for (SubscriptionInfo subInfo : subInfoList) {
+                            SharedPreferences spPrefFile = getContext().getSharedPreferences(
+                                    PREF_FILE_APN + subInfo.getSubscriptionId(), Context.MODE_PRIVATE);
+                            if (spPrefFile != null) {
+                                SharedPreferences.Editor editor = spPrefFile.edit();
+                                editor.clear();
+                                editor.apply();
+                            }
                         }
                     }
-                }
 
-                // Update APN DB
-                updateApnDb();
+                    // Update APN DB
+                    updateApnDb();
+                } else {
+                    if (VDBG) log("onCreate: build id did not change: " + oldBuildId);
+                }
+                sp.edit().putString(RO_BUILD_ID, newBuildId).apply();
             } else {
-                if (VDBG) log("onCreate: build id did not change: " + oldBuildId);
+                if (VDBG) log("onCreate: newBuildId is empty");
             }
-            sp.edit().putString(RO_BUILD_ID, newBuildId).apply();
-        } else {
-            if (VDBG) log("onCreate: newBuildId is empty");
         }
 
         if (VDBG) log("onCreate:- ret true");
@@ -1911,8 +2094,19 @@
         }
     }
 
+    /**
+     * Insert an array of ContentValues and call notifyChange at the end.
+     */
     @Override
     public synchronized int bulkInsert(Uri url, ContentValues[] values) {
+        return unsynchronizedBulkInsert(url, values);
+    }
+
+    /**
+     * Do a bulk insert while inside a synchronized function. This is typically not safe and should
+     * only be done when you are sure there will be no conflict.
+     */
+    private int unsynchronizedBulkInsert(Uri url, ContentValues[] values) {
         int count = 0;
         boolean notify = false;
         for (ContentValues value : values) {
@@ -2412,10 +2606,19 @@
         editorApn.clear();
         editorApn.apply();
 
-        initDatabaseWithDatabaseHelper(db);
+        if (apnSourceServiceExists(getContext())) {
+            restoreApnsWithService();
+        } else {
+            initDatabaseWithDatabaseHelper(db);
+        }
     }
 
     private synchronized void updateApnDb() {
+        if (apnSourceServiceExists(getContext())) {
+            loge("called updateApnDb when apn source service exists");
+            return;
+        }
+
         if (!needApnDbUpdate()) {
             log("Skipping apn db update since apn-conf has not changed.");
             return;
diff --git a/tests/src/com/android/providers/telephony/CarrierProviderTest.java b/tests/src/com/android/providers/telephony/CarrierProviderTest.java
new file mode 100644
index 0000000..6a56343
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/CarrierProviderTest.java
@@ -0,0 +1,282 @@
+/*
+ * 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.providers.telephony;
+
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.providers.telephony.CarrierProvider;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+
+/**
+ * Tests for testing CRUD operations of CarrierProvider.
+ * Uses TelephonyProviderTestable to set up in-memory database
+ *
+ * Build, install and run the tests by running the commands below:
+ *     runtest --path <dir or file>
+ *     runtest --path <dir or file> --test-method <testMethodName>
+ *     e.g.)
+ *         runtest --path tests/src/com/android/providers/telephony/CarrierProviderTest.java \
+ *                 --test-method testInsertCarriers
+ */
+public class CarrierProviderTest extends TestCase {
+
+    private static final String TAG = "CarrierProviderTest";
+
+    private MockContextWithProvider mContext;
+    private MockContentResolver mContentResolver;
+    private CarrierProviderTestable mCarrierProviderTestable;
+
+    public static final String dummy_type = "TYPE5";
+    public static final String dummy_mnc = "MNC001";
+    public static final String dummy_mnc2 = "MNC002";
+    public static final String dummy_mcc = "MCC005";
+    public static final String dummy_key1 = "PUBKEY1";
+    public static final String dummy_key2 = "PUBKEY2";
+    public static final String dummy_mvno_type = "100";
+    public static final String dummy_mvno_match_data = "101";
+
+
+    /**
+     * This is used to give the CarrierProviderTest a mocked context which takes a
+     * CarrierProvider and attaches it to the ContentResolver.
+     */
+    private class MockContextWithProvider extends MockContext {
+        private final MockContentResolver mResolver;
+
+        public MockContextWithProvider(CarrierProvider carrierProvider) {
+            mResolver = new MockContentResolver();
+
+            ProviderInfo providerInfo = new ProviderInfo();
+            providerInfo.authority = CarrierProvider.PROVIDER_NAME;
+
+            // Add context to given telephonyProvider
+            carrierProvider.attachInfoForTesting(this, providerInfo);
+            Log.d(TAG, "MockContextWithProvider: carrierProvider.getContext(): "
+                    + carrierProvider.getContext());
+
+            // Add given telephonyProvider to mResolver, so that mResolver can send queries
+            // to the provider.
+            mResolver.addProvider(CarrierProvider.PROVIDER_NAME, carrierProvider);
+            Log.d(TAG, "MockContextWithProvider: Add carrierProvider to mResolver");
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            Log.d(TAG, "getSystemService: returning null");
+            return null;
+        }
+
+        @Override
+        public Resources getResources() {
+            Log.d(TAG, "getResources: returning null");
+            return null;
+        }
+
+        @Override
+        public MockContentResolver getContentResolver() {
+            return mResolver;
+        }
+
+        // Gives permission to write to the APN table within the MockContext
+        @Override
+        public int checkCallingOrSelfPermission(String permission) {
+            if (TextUtils.equals(permission, "android.permission.WRITE_APN_SETTINGS")) {
+                Log.d(TAG, "checkCallingOrSelfPermission: permission=" + permission
+                        + ", returning PackageManager.PERMISSION_GRANTED");
+                return PackageManager.PERMISSION_GRANTED;
+            } else {
+                Log.d(TAG, "checkCallingOrSelfPermission: permission=" + permission
+                        + ", returning PackageManager.PERMISSION_DENIED");
+                return PackageManager.PERMISSION_DENIED;
+            }
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mCarrierProviderTestable = new CarrierProviderTestable();
+        mContext = new MockContextWithProvider(mCarrierProviderTestable);
+        mContentResolver = (MockContentResolver) mContext.getContentResolver();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        mCarrierProviderTestable.closeDatabase();
+    }
+
+    /**
+     * Test inserting values in carrier key table.
+     */
+    @Test
+    @SmallTest
+    public void testInsertCertificates() {
+        int count = -1;
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(CarrierDatabaseHelper.KEY_TYPE, dummy_type);
+        contentValues.put(CarrierDatabaseHelper.MCC, dummy_mcc);
+        contentValues.put(CarrierDatabaseHelper.MNC, dummy_mnc);
+        contentValues.put(CarrierDatabaseHelper.MVNO_TYPE, dummy_mvno_type);
+        contentValues.put(CarrierDatabaseHelper.MVNO_MATCH_DATA, dummy_mvno_match_data);
+        contentValues.put(CarrierDatabaseHelper.PUBLIC_CERTIFICATE, dummy_key1);
+
+        try {
+            mContentResolver.insert(CarrierProvider.CONTENT_URI, contentValues);
+        } catch (Exception e) {
+            Log.d(TAG, "Error inserting certificates:" + e);
+        }
+        try {
+            Cursor countCursor = mContentResolver.query(CarrierProvider.CONTENT_URI,
+                    new String[]{"count(*) AS count"},
+                    null,
+                    null,
+                    null);
+            countCursor.moveToFirst();
+            count = countCursor.getInt(0);
+        } catch (Exception e) {
+            Log.d(TAG, "Exception in getting count:" + e);
+        }
+        assertEquals(1, count);
+    }
+
+    /**
+     * Test update & query.
+     */
+    @Test
+    @SmallTest
+    public void testUpdateCertificates() {
+        String key = null;
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(CarrierDatabaseHelper.KEY_TYPE, dummy_type);
+        contentValues.put(CarrierDatabaseHelper.MCC, dummy_mcc);
+        contentValues.put(CarrierDatabaseHelper.MNC, dummy_mnc);
+        contentValues.put(CarrierDatabaseHelper.MVNO_TYPE, dummy_mvno_type);
+        contentValues.put(CarrierDatabaseHelper.MVNO_MATCH_DATA, dummy_mvno_match_data);
+        contentValues.put(CarrierDatabaseHelper.PUBLIC_CERTIFICATE, dummy_key1);
+
+        try {
+            mContentResolver.insert(CarrierProvider.CONTENT_URI, contentValues);
+        } catch (Exception e) {
+            Log.d(TAG, "Error inserting certificates:" + e);
+        }
+
+        try {
+            ContentValues updatedValues = new ContentValues();
+            updatedValues.put(CarrierDatabaseHelper.PUBLIC_CERTIFICATE, dummy_key2);
+            mContentResolver.update(CarrierProvider.CONTENT_URI, updatedValues,
+                    "mcc=? and mnc=? and key_type=?", new String[] { dummy_mcc, dummy_mnc, dummy_type });
+        } catch (Exception e) {
+            Log.d(TAG, "Error updating values:" + e);
+        }
+
+        try {
+            String[] columns ={CarrierDatabaseHelper.PUBLIC_CERTIFICATE};
+            Cursor findEntry = mContentResolver.query(CarrierProvider.CONTENT_URI, columns,
+                    "mcc=? and mnc=? and key_type=?",
+                    new String[] { dummy_mcc, dummy_mnc, dummy_type }, null);
+            findEntry.moveToFirst();
+            key = findEntry.getString(0);
+        } catch (Exception e) {
+            Log.d(TAG, "Query failed:" + e);
+        }
+        assertEquals(key, dummy_key2);
+    }
+
+    /**
+     * Test inserting multiple certs
+     */
+    @Test
+    @SmallTest
+    public void testMultipleCertificates() {
+        int count = -1;
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(CarrierDatabaseHelper.KEY_TYPE, dummy_type);
+        contentValues.put(CarrierDatabaseHelper.MCC, dummy_mcc);
+        contentValues.put(CarrierDatabaseHelper.MNC, dummy_mnc);
+        contentValues.put(CarrierDatabaseHelper.MVNO_TYPE, dummy_mvno_type);
+        contentValues.put(CarrierDatabaseHelper.MVNO_MATCH_DATA, dummy_mvno_match_data);
+        contentValues.put(CarrierDatabaseHelper.PUBLIC_CERTIFICATE, dummy_key1);
+
+        ContentValues contentValuesNew = new ContentValues();
+        contentValuesNew.put(CarrierDatabaseHelper.KEY_TYPE, dummy_type);
+        contentValuesNew.put(CarrierDatabaseHelper.MCC, dummy_mcc);
+        contentValuesNew.put(CarrierDatabaseHelper.MNC, dummy_mnc2);
+        contentValuesNew.put(CarrierDatabaseHelper.MVNO_TYPE, dummy_mvno_type);
+        contentValuesNew.put(CarrierDatabaseHelper.MVNO_MATCH_DATA, dummy_mvno_match_data);
+        contentValuesNew.put(CarrierDatabaseHelper.PUBLIC_CERTIFICATE, dummy_key2);
+
+        try {
+            mContentResolver.insert(CarrierProvider.CONTENT_URI, contentValues);
+            mContentResolver.insert(CarrierProvider.CONTENT_URI, contentValuesNew);
+        } catch (Exception e) {
+            System.out.println("Error inserting certificates:: " + e);
+        }
+
+        try {
+            Cursor countCursor = mContentResolver.query(CarrierProvider.CONTENT_URI,
+                    new String[]{"count(*) AS count"},
+                    null,
+                    null,
+                    null);
+            countCursor.moveToFirst();
+            count = countCursor.getInt(0);
+        } catch (Exception e) {
+            Log.d(TAG, "Exception in getting count:" + e);
+        }
+        assertEquals(2, count);
+    }
+
+    /**
+     * Test inserting duplicate values in carrier key table. Ensure that a SQLException is thrown.
+     */
+    @Test(expected = SQLException.class)
+    public void testDuplicateFailure() {
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(CarrierDatabaseHelper.KEY_TYPE, dummy_type);
+        contentValues.put(CarrierDatabaseHelper.MCC, dummy_mcc);
+        contentValues.put(CarrierDatabaseHelper.MNC, dummy_mnc);
+        contentValues.put(CarrierDatabaseHelper.MVNO_TYPE, dummy_mvno_type);
+        contentValues.put(CarrierDatabaseHelper.MVNO_MATCH_DATA, dummy_mvno_match_data);
+        contentValues.put(CarrierDatabaseHelper.PUBLIC_CERTIFICATE, dummy_key1);
+
+        try {
+            mContentResolver.insert(CarrierProvider.CONTENT_URI, contentValues);
+        } catch (Exception e) {
+            Log.d(TAG, "Error inserting certificates:: " + e);
+        }
+        try {
+            mContentResolver.insert(CarrierProvider.CONTENT_URI, contentValues);
+        } catch (Exception e) {
+            Log.d(TAG, "Error inserting certificates:: " + e);
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/telephony/CarrierProviderTestable.java b/tests/src/com/android/providers/telephony/CarrierProviderTestable.java
new file mode 100644
index 0000000..87d6c5f
--- /dev/null
+++ b/tests/src/com/android/providers/telephony/CarrierProviderTestable.java
@@ -0,0 +1,85 @@
+/*
+ * 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.providers.telephony;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import com.android.providers.telephony.CarrierProvider;
+import static com.android.providers.telephony.CarrierDatabaseHelper.*;
+
+/**
+ * A subclass of TelephonyProvider used for testing on an in-memory database
+ */
+public class CarrierProviderTestable extends CarrierProvider {
+    private static final String TAG = "CarrierProviderTestable";
+
+    private InMemoryCarrierProviderDbHelper mDbHelper;
+
+    @Override
+    public boolean onCreate() {
+        Log.d(TAG, "onCreate called: mDbHelper = new InMemoryCarrierProviderDbHelper()");
+        mDbHelper = new InMemoryCarrierProviderDbHelper();
+        return true;
+    }
+
+    // close mDbHelper database object
+    protected void closeDatabase() {
+        mDbHelper.close();
+    }
+
+    @Override
+    SQLiteDatabase getReadableDatabase() {
+        Log.d(TAG, "getReadableDatabase called" + mDbHelper.getReadableDatabase());
+        return mDbHelper.getReadableDatabase();
+    }
+
+    @Override
+    SQLiteDatabase getWritableDatabase() {
+        Log.d(TAG, "getWritableDatabase called" + mDbHelper.getWritableDatabase());
+        return mDbHelper.getWritableDatabase();
+    }
+
+    /**
+     * An in memory DB for CarrierProviderTestable to use
+     */
+    public static class InMemoryCarrierProviderDbHelper extends SQLiteOpenHelper {
+
+
+        public InMemoryCarrierProviderDbHelper() {
+            super(null,      // no context is needed for in-memory db
+                    null,    // db file name is null for in-memory db
+                    null,    // CursorFactory is null by default
+                    1);      // db version is no-op for tests
+            Log.d(TAG, "InMemoryCarrierProviderDbHelper creating in-memory database");
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+
+            //set up the Carrier key table
+            Log.d(TAG, "InMemoryCarrierProviderDbHelper onCreate creating the carrier key table");
+            db.execSQL(getStringForCarrierKeyTableCreation(CARRIER_KEY_TABLE));
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            Log.d(TAG, "InMemoryCarrierProviderDbHelper onUpgrade doing nothing");
+            return;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java b/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
index a5dcff7..49106ee 100644
--- a/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
+++ b/tests/src/com/android/providers/telephony/TelephonyBackupAgentTest.java
@@ -32,10 +32,12 @@
 import android.test.mock.MockContentProvider;
 import android.test.mock.MockContentResolver;
 import android.test.mock.MockCursor;
+import android.text.TextUtils;
 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 org.json.JSONArray;
@@ -53,13 +55,14 @@
 import java.util.Set;
 import java.util.UUID;
 
-
 /**
  * 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.
+ *
+ * To run this test from the android root: runtest --path packages/providers/TelephonyProvider/
  */
-@TargetApi(Build.VERSION_CODES.M)
+@TargetApi(Build.VERSION_CODES.O)
 public class TelephonyBackupAgentTest extends AndroidTestCase {
     /* Map subscriptionId -> phone number */
     private SparseArray<String> mSubId2Phone;
@@ -74,11 +77,11 @@
     /* Cursors being used to access sms, mms tables */
     private FakeCursor mSmsCursor, mMmsCursor;
     /* Test data with sms and mms */
-    private ContentValues[] mSmsRows, mMmsRows;
+    private ContentValues[] mSmsRows, mMmsRows, mMmsAttachmentRows;
     /* Json representation for the test data */
-    private String[] mSmsJson, mMmsJson;
+    private String[] mSmsJson, mMmsJson, mMmsAttachmentJson;
     /* sms, mms json concatenated as json array */
-    private String mAllSmsJson, mAllMmsJson;
+    private String mAllSmsJson, mAllMmsJson, mMmsAllAttachmentJson;
 
     private StringWriter mStringWriter;
 
@@ -121,36 +124,36 @@
         mSmsRows = new ContentValues[4];
         mSmsJson = new String[4];
         mSmsRows[0] = createSmsRow(1, 1, "+1232132214124", "sms 1", "sms subject", 9087978987l,
-                999999999, 3, 44, 1);
+                999999999, 3, 44, 1, false);
         mSmsJson[0] = "{\"self_phone\":\"+111111111111111\",\"address\":" +
                 "\"+1232132214124\",\"body\":\"sms 1\",\"subject\":\"sms subject\",\"date\":" +
                 "\"9087978987\",\"date_sent\":\"999999999\",\"status\":\"3\",\"type\":\"44\"," +
-                "\"recipients\":[\"+123 (213) 2214124\"],\"archived\":true}";
+                "\"recipients\":[\"+123 (213) 2214124\"],\"archived\":true,\"read\":\"0\"}";
         mThreadProvider.setArchived(
                 mThreadProvider.getOrCreateThreadId(new String[]{"+123 (213) 2214124"}));
 
         mSmsRows[1] = createSmsRow(2, 2, "+1232132214124", "sms 2", null, 9087978987l, 999999999,
-                0, 4, 1);
+                0, 4, 1, true);
         mSmsJson[1] = "{\"address\":\"+1232132214124\",\"body\":\"sms 2\",\"date\":" +
                 "\"9087978987\",\"date_sent\":\"999999999\",\"status\":\"0\",\"type\":\"4\"," +
-                "\"recipients\":[\"+123 (213) 2214124\"]}";
+                "\"recipients\":[\"+123 (213) 2214124\"],\"read\":\"1\"}";
 
         mSmsRows[2] = createSmsRow(4, 3, "+1232221412433 +1232221412444", "sms 3", null,
-                111111111111l, 999999999, 2, 3, 2);
+                111111111111l, 999999999, 2, 3, 2, false);
         mSmsJson[2] =  "{\"self_phone\":\"+333333333333333\",\"address\":" +
                 "\"+1232221412433 +1232221412444\",\"body\":\"sms 3\",\"date\":\"111111111111\"," +
                 "\"date_sent\":" +
                 "\"999999999\",\"status\":\"2\",\"type\":\"3\"," +
-                "\"recipients\":[\"+1232221412433\",\"+1232221412444\"]}";
+                "\"recipients\":[\"+1232221412433\",\"+1232221412444\"],\"read\":\"0\"}";
         mThreadProvider.getOrCreateThreadId(new String[]{"+1232221412433", "+1232221412444"});
 
 
         mSmsRows[3] = createSmsRow(5, 3, null, "sms 4", null,
-                111111111111l, 999999999, 2, 3, 5);
+                111111111111l, 999999999, 2, 3, 5, false);
         mSmsJson[3] = "{\"self_phone\":\"+333333333333333\"," +
                 "\"body\":\"sms 4\",\"date\":\"111111111111\"," +
                 "\"date_sent\":" +
-                "\"999999999\",\"status\":\"2\",\"type\":\"3\"}";
+                "\"999999999\",\"status\":\"2\",\"type\":\"3\",\"read\":\"0\"}";
 
         mAllSmsJson = makeJsonArray(mSmsJson);
 
@@ -165,12 +168,14 @@
                 111 /*body charset*/,
                 new String[]{"+111 (111) 11111111", "+11121212", "example@example.com",
                         "+999999999"} /*addresses*/,
-                3 /*threadId*/);
+                3 /*threadId*/, false /*read*/, null /*smil*/, null /*attachmentTypes*/,
+                null /*attachmentFilenames*/);
 
         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\"," +
                 "\"recipients\":[\"+11121212\",\"example@example.com\",\"+999999999\"]," +
+                "\"read\":\"0\"," +
                 "\"mms_addresses\":" +
                 "[{\"type\":10,\"address\":\"+111 (111) 11111111\",\"charset\":100}," +
                 "{\"type\":11,\"address\":\"+11121212\",\"charset\":101},{\"type\":12,\"address\":"+
@@ -185,10 +190,12 @@
                 222 /*msgBox*/, "location 2" /*contentLocation*/, "MMs body 2" /*body*/,
                 121 /*body charset*/,
                 new String[]{"+7 (333) ", "example@example.com", "+999999999"} /*addresses*/,
-                4 /*threadId*/);
+                4 /*threadId*/, true /*read*/, null /*smil*/, null /*attachmentTypes*/,
+                null /*attachmentFilenames*/);
         mMmsJson[1] = "{\"date\":\"111122\",\"date_sent\":\"1111112\",\"m_type\":\"4\"," +
                 "\"v\":\"18\",\"msg_box\":\"222\",\"ct_l\":\"location 2\"," +
                 "\"recipients\":[\"example@example.com\",\"+999999999\"]," +
+                "\"read\":\"1\"," +
                 "\"mms_addresses\":" +
                 "[{\"type\":10,\"address\":\"+7 (333) \",\"charset\":100}," +
                 "{\"type\":11,\"address\":\"example@example.com\",\"charset\":101}," +
@@ -202,12 +209,14 @@
                 333 /*msgBox*/, null /*contentLocation*/, "MMs body 3" /*body*/,
                 131 /*body charset*/,
                 new String[]{"333 333333333333", "+1232132214124"} /*addresses*/,
-                1 /*threadId*/);
+                1 /*threadId*/, false /*read*/, null /*smil*/, null /*attachmentTypes*/,
+                null /*attachmentFilenames*/);
 
         mMmsJson[2] = "{\"self_phone\":\"+333333333333333\",\"sub\":\"Subject 10\"," +
                 "\"date\":\"111133\",\"date_sent\":\"1111132\",\"m_type\":\"5\",\"v\":\"19\"," +
                 "\"msg_box\":\"333\"," +
                 "\"recipients\":[\"+123 (213) 2214124\"],\"archived\":true," +
+                "\"read\":\"0\"," +
                 "\"mms_addresses\":" +
                 "[{\"type\":10,\"address\":\"333 333333333333\",\"charset\":100}," +
                 "{\"type\":11,\"address\":\"+1232132214124\",\"charset\":101}]," +
@@ -215,6 +224,38 @@
                 "\"sub_cs\":\"10\"}";
         mAllMmsJson = makeJsonArray(mMmsJson);
 
+
+        mMmsAttachmentRows = new ContentValues[1];
+        mMmsAttachmentJson = new String[1];
+        mMmsAttachmentRows[0] = createMmsRow(1 /*id*/, 1 /*subid*/, "Subject 1" /*subject*/,
+                100 /*subcharset*/, 111111 /*date*/, 111112 /*datesent*/, 3 /*type*/,
+                17 /*version*/, 0 /*textonly*/,
+                11 /*msgBox*/, "location 1" /*contentLocation*/, "MMs body 1" /*body*/,
+                111 /*body charset*/,
+                new String[]{"+111 (111) 11111111", "+11121212", "example@example.com",
+                        "+999999999"} /*addresses*/,
+                3 /*threadId*/, false /*read*/, "<smil><head><layout><root-layout/>"
+                        + "<region id='Image' fit='meet' top='0' left='0' height='100%'"
+                        + " width='100%'/></layout></head><body><par dur='5000ms'>"
+                        + "<img src='image000000.jpg' region='Image' /></par></body></smil>",
+                new String[] {"image/jpg"} /*attachmentTypes*/,
+                new String[] {"GreatPict.jpg"}  /*attachmentFilenames*/);
+
+        mMmsAttachmentJson[0] = "{\"self_phone\":\"+111111111111111\",\"sub\":\"Subject 1\"," +
+                "\"date\":\"111111\",\"date_sent\":\"111112\",\"m_type\":\"3\",\"v\":\"17\"," +
+                "\"msg_box\":\"11\",\"ct_l\":\"location 1\"," +
+                "\"recipients\":[\"+11121212\",\"example@example.com\",\"+999999999\"]," +
+                "\"read\":\"0\"," +
+                "\"mms_addresses\":" +
+                "[{\"type\":10,\"address\":\"+111 (111) 11111111\",\"charset\":100}," +
+                "{\"type\":11,\"address\":\"+11121212\",\"charset\":101},{\"type\":12,\"address\":"+
+                "\"example@example.com\",\"charset\":102},{\"type\":13,\"address\":\"+999999999\"" +
+                ",\"charset\":103}],\"mms_body\":\"MMs body 1\",\"mms_charset\":111,\"" +
+                "sub_cs\":\"100\"}";
+
+        mMmsAllAttachmentJson = makeJsonArray(mMmsAttachmentJson);
+
+
         ContentProvider contentProvider = new MockContentProvider() {
             @Override
             public Cursor query(Uri uri, String[] projection, String selection,
@@ -270,7 +311,8 @@
 
     private static ContentValues createSmsRow(int id, int subId, String address, String body,
                                               String subj, long date, long dateSent,
-                                              int status, int type, long threadId) {
+                                              int status, int type, long threadId,
+                                              boolean read) {
         ContentValues smsRow = new ContentValues();
         smsRow.put(Telephony.Sms._ID, id);
         smsRow.put(Telephony.Sms.SUBSCRIPTION_ID, subId);
@@ -288,6 +330,7 @@
         smsRow.put(Telephony.Sms.STATUS, String.valueOf(status));
         smsRow.put(Telephony.Sms.TYPE, String.valueOf(type));
         smsRow.put(Telephony.Sms.THREAD_ID, threadId);
+        smsRow.put(Telephony.Sms.READ, read ? "1" : "0");
 
         return smsRow;
     }
@@ -296,7 +339,9 @@
                                        long date, long dateSent, int type, int version,
                                        int textOnly, int msgBox,
                                        String contentLocation, String body,
-                                       int bodyCharset, String[] addresses, long threadId) {
+                                       int bodyCharset, String[] addresses, long threadId,
+                                       boolean read, String smil, String[] attachmentTypes,
+                                       String[] attachmentFilenames) {
         ContentValues mmsRow = new ContentValues();
         mmsRow.put(Telephony.Mms._ID, id);
         mmsRow.put(Telephony.Mms.SUBSCRIPTION_ID, subId);
@@ -314,10 +359,12 @@
             mmsRow.put(Telephony.Mms.CONTENT_LOCATION, contentLocation);
         }
         mmsRow.put(Telephony.Mms.THREAD_ID, threadId);
+        mmsRow.put(Telephony.Mms.READ, read ? "1" : "0");
 
         final Uri partUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).
                 appendPath("part").build();
-        mCursors.put(partUri, createBodyCursor(body, bodyCharset));
+        mCursors.put(partUri, createBodyCursor(body, bodyCharset, smil, attachmentTypes,
+                attachmentFilenames));
         mMmsAllContentValues.add(mmsRow);
 
         final Uri addrUri = Telephony.Mms.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).
@@ -329,14 +376,18 @@
 
     private static final String APP_SMIL = "application/smil";
     private static final String TEXT_PLAIN = "text/plain";
+    private static final String IMAGE_JPG = "image/jpg";
 
     // Cursor with parts of Mms.
-    private FakeCursor createBodyCursor(String body, int charset) {
+    private FakeCursor createBodyCursor(String body, int charset, String existingSmil,
+            String[] attachmentTypes, String[] attachmentFilenames) {
         List<ContentValues> table = new ArrayList<>();
         final String srcName = String.format("text.%06d.txt", 0);
-        final String smilBody = String.format(TelephonyBackupAgent.sSmilTextPart, srcName);
+        final String smilBody = TextUtils.isEmpty(existingSmil) ?
+                String.format(TelephonyBackupAgent.sSmilTextPart, srcName) : existingSmil;
         final String smil = String.format(TelephonyBackupAgent.sSmilTextOnly, smilBody);
 
+        // SMIL
         final ContentValues smilPart = new ContentValues();
         smilPart.put(Telephony.Mms.Part.SEQ, -1);
         smilPart.put(Telephony.Mms.Part.CONTENT_TYPE, APP_SMIL);
@@ -346,6 +397,7 @@
         smilPart.put(Telephony.Mms.Part.TEXT, smil);
         mMmsAllContentValues.add(smilPart);
 
+        // Text part
         final ContentValues bodyPart = new ContentValues();
         bodyPart.put(Telephony.Mms.Part.SEQ, 0);
         bodyPart.put(Telephony.Mms.Part.CONTENT_TYPE, TEXT_PLAIN);
@@ -357,6 +409,22 @@
         table.add(bodyPart);
         mMmsAllContentValues.add(bodyPart);
 
+        // Attachments
+        if (attachmentTypes != null) {
+            for (int i = 0; i < attachmentTypes.length; i++) {
+                String attachmentType = attachmentTypes[i];
+                String attachmentFilename = attachmentFilenames[i];
+                final ContentValues attachmentPart = new ContentValues();
+                attachmentPart.put(Telephony.Mms.Part.SEQ, i + 1);
+                attachmentPart.put(Telephony.Mms.Part.CONTENT_TYPE, attachmentType);
+                attachmentPart.put(Telephony.Mms.Part.NAME, attachmentFilename);
+                attachmentPart.put(Telephony.Mms.Part.CONTENT_ID, "<"+attachmentFilename+">");
+                attachmentPart.put(Telephony.Mms.Part.CONTENT_LOCATION, attachmentFilename);
+                table.add(attachmentPart);
+                mMmsAllContentValues.add(attachmentPart);
+            }
+        }
+
         return new FakeCursor(table, TelephonyBackupAgent.MMS_TEXT_PROJECTION);
     }
 
@@ -450,6 +518,17 @@
     }
 
     /**
+     * Test with attachment mms.
+     * @throws Exception
+     */
+    public void testBackupMmsWithAttachmentMms() throws Exception {
+        mTelephonyBackupAgent.mMaxMsgPerFile = 4;
+        mMmsTable.addAll(Arrays.asList(mMmsAttachmentRows));
+        mTelephonyBackupAgent.putMmsMessagesToJson(mMmsCursor, new JsonWriter(mStringWriter));
+        assertEquals(mMmsAllAttachmentJson, mStringWriter.toString());
+    }
+
+    /**
      * Test with 3 mms in the provider with the limit per file 1.
      * @throws Exception
      */
@@ -518,7 +597,7 @@
     }
 
     /**
-     * Test restore sms with three mms json object in the array.
+     * Test restore mms with three mms json object in the array.
      * @throws Exception
      */
     public void testRestoreMms_AllMms() throws Exception {
@@ -531,6 +610,19 @@
     }
 
     /**
+     * Test restore a single mms with an attachment.
+     * @throws Exception
+     */
+    public void testRestoreMmsWithAttachment() throws Exception {
+        JsonReader jsonReader = new JsonReader
+                (new StringReader(addRandomDataToJson(mMmsAllAttachmentJson)));
+        FakeMmsProvider mmsProvider = new FakeMmsProvider(mMmsAllContentValues);
+        mMockContentResolver.addProvider("mms", mmsProvider);
+        mTelephonyBackupAgent.putMmsMessagesToProvider(jsonReader);
+        assertEquals(7, mmsProvider.getRowsAdded());
+    }
+
+    /**
      * Test with quota exceeded. Checking size of the backup before it hits quota and after.
      * It still backs up more than a quota since there is meta-info which matters with small amounts
      * of data. The agent does not take backup meta-info into consideration.
@@ -589,7 +681,6 @@
             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) {
@@ -631,6 +722,7 @@
         private List<ContentValues> mValues;
         private long mDummyMsgId = -1;
         private long mMsgId = -1;
+        private String mFilename;
 
         public FakeMmsProvider(List<ContentValues> values) {
             this.mValues = values;
@@ -640,11 +732,29 @@
         public Uri insert(Uri uri, ContentValues values) {
             Uri retUri = Uri.parse("dummy_uri");
             ContentValues modifiedValues = new ContentValues(mValues.get(nextRow++));
+            if (values.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
+            if (modifiedValues.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
             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 (IMAGE_JPG.equals(values.get(Telephony.Mms.Part.CONTENT_TYPE))) {
+                // Image attachment part.
+                mFilename = values.getAsString(Telephony.Mms.Part.CONTENT_LOCATION);
+                String path = values.getAsString(Telephony.Mms.Part._DATA);
+                assertTrue(path.endsWith(mFilename));
+            }
+            if (values.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
+            if (modifiedValues.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
 
             if (values.get(Telephony.Mms.Part.SEQ) != null) {
                 // Part of mms.
@@ -654,10 +764,22 @@
                         .build();
                 assertEquals(expectedUri, uri);
             }
+            if (values.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
+            if (modifiedValues.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
 
             if (values.get(Telephony.Mms.Part.MSG_ID) != null) {
                 modifiedValues.put(Telephony.Mms.Part.MSG_ID, mDummyMsgId);
             }
+            if (values.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
+            if (modifiedValues.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
 
 
             if (values.get(Telephony.Mms.SUBSCRIPTION_ID) != null) {
@@ -667,12 +789,17 @@
                     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.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
+            if (modifiedValues.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
 
             if (values.get(Telephony.Mms.Addr.ADDRESS) != null) {
                 // Address.
@@ -685,6 +812,12 @@
                 modifiedValues.put(Telephony.Mms.Addr.MSG_ID, mMsgId);
                 mDummyMsgId = -1;
             }
+            if (values.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
+            if (modifiedValues.containsKey("read")) {
+                assertEquals("read: ", modifiedValues.get("read"), values.get("read"));
+            }
 
             for (String key : modifiedValues.keySet()) {
                 assertEquals("Key:"+key, modifiedValues.get(key), values.get(key));