auto import from //depot/cupcake/@135843
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..2b16664
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,13 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user development
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_JAVA_LIBRARIES := 
+
+LOCAL_PACKAGE_NAME := TelephonyProvider
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..c1af548
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.providers.telephony"
+        android:sharedUserId="android.uid.phone"
+>
+
+    <application android:process="com.android.phone"
+                 android:allowClearUserData="false"
+                 android:label="Dialer Storage"
+                 android:icon="@drawable/ic_launcher_phone">
+        <provider android:name="TelephonyProvider" android:authorities="telephony" android:multiprocess="true" />
+        <provider android:name="SmsProvider" android:authorities="sms" android:multiprocess="true" 
+            android:readPermission="android.permission.READ_SMS"
+            android:writePermission="android.permission.WRITE_SMS" />
+        <provider android:name="MmsProvider" android:authorities="mms" android:multiprocess="true" 
+            android:readPermission="android.permission.READ_SMS"
+            android:writePermission="android.permission.WRITE_SMS">
+            <grant-uri-permission android:pathPrefix="/part/" />
+        </provider>
+        <provider android:name="MmsSmsProvider" android:authorities="mms-sms" android:multiprocess="true" 
+            android:readPermission="android.permission.READ_SMS"
+            android:writePermission="android.permission.WRITE_SMS" />
+    </application>
+</manifest>
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2008, 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.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/res/drawable/ic_launcher_phone.png b/res/drawable/ic_launcher_phone.png
new file mode 100644
index 0000000..4e613ec
--- /dev/null
+++ b/res/drawable/ic_launcher_phone.png
Binary files differ
diff --git a/src/com/android/providers/telephony/MmsProvider.java b/src/com/android/providers/telephony/MmsProvider.java
new file mode 100644
index 0000000..37d1787
--- /dev/null
+++ b/src/com/android/providers/telephony/MmsProvider.java
@@ -0,0 +1,796 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.telephony;
+
+import com.google.android.mms.pdu.PduHeaders;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.Telephony.Mms;
+import android.provider.Telephony.MmsSms;
+import android.provider.Telephony.Mms.Addr;
+import android.provider.Telephony.Mms.Part;
+import android.provider.Telephony.Mms.Rate;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/**
+ * The class to provide base facility to access MMS related content,
+ * which is stored in a SQLite database and in the file system.
+ */
+public class MmsProvider extends ContentProvider {
+    static final String TABLE_PDU  = "pdu";
+    static final String TABLE_ADDR = "addr";
+    static final String TABLE_PART = "part";
+    static final String TABLE_RATE = "rate";
+    static final String TABLE_DRM  = "drm";
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = MmsSmsDatabaseHelper.getInstance(getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+        // Generate the body of the query.
+        int match = sURLMatcher.match(uri);
+        if (LOCAL_LOGV) {
+            Log.v(TAG, "Query uri=" + uri + ", match=" + match);
+        }
+
+        switch (match) {
+            case MMS_ALL:
+                constructQueryForBox(qb, Mms.MESSAGE_BOX_ALL);
+                break;
+            case MMS_INBOX:
+                constructQueryForBox(qb, Mms.MESSAGE_BOX_INBOX);
+                break;
+            case MMS_SENT:
+                constructQueryForBox(qb, Mms.MESSAGE_BOX_SENT);
+                break;
+            case MMS_DRAFTS:
+                constructQueryForBox(qb, Mms.MESSAGE_BOX_DRAFTS);
+                break;
+            case MMS_OUTBOX:
+                constructQueryForBox(qb, Mms.MESSAGE_BOX_OUTBOX);
+                break;
+            case MMS_ALL_ID:
+                qb.setTables(TABLE_PDU);
+                qb.appendWhere(Mms._ID + "=" + uri.getPathSegments().get(0));
+                break;
+            case MMS_INBOX_ID:
+            case MMS_SENT_ID:
+            case MMS_DRAFTS_ID:
+            case MMS_OUTBOX_ID:
+                qb.setTables(TABLE_PDU);
+                qb.appendWhere(Mms._ID + "=" + uri.getPathSegments().get(1));
+                qb.appendWhere(" AND " + Mms.MESSAGE_BOX + "="
+                        + getMessageBoxByMatch(match));
+                break;
+            case MMS_ALL_PART:
+                qb.setTables(TABLE_PART);
+                break;
+            case MMS_MSG_PART:
+                qb.setTables(TABLE_PART);
+                qb.appendWhere(Part.MSG_ID + "=" + uri.getPathSegments().get(0));
+                break;
+            case MMS_PART_ID:
+                qb.setTables(TABLE_PART);
+                qb.appendWhere(Part._ID + "=" + uri.getPathSegments().get(1));
+                break;
+            case MMS_MSG_ADDR:
+                qb.setTables(TABLE_ADDR);
+                qb.appendWhere(Addr.MSG_ID + "=" + uri.getPathSegments().get(0));
+                break;
+            case MMS_REPORT_STATUS:
+                /*
+                   SELECT DISTINCT address,
+                                   T.delivery_status AS delivery_status,
+                                   T.read_status AS read_status
+                   FROM addr
+                   INNER JOIN (SELECT P1._id AS id1, P2._id AS id2, P3._id AS id3,
+                                      ifnull(P2.st, 0) AS delivery_status,
+                                      ifnull(P3.read_status, 0) AS read_status
+                               FROM pdu P1
+                               INNER JOIN pdu P2
+                               ON P1.m_id = P2.m_id AND P2.m_type = 134
+                               LEFT JOIN pdu P3
+                               ON P1.m_id = P3.m_id AND P3.m_type = 136
+                               UNION
+                               SELECT P1._id AS id1, P2._id AS id2, P3._id AS id3,
+                                      ifnull(P2.st, 0) AS delivery_status,
+                                      ifnull(P3.read_status, 0) AS read_status
+                               FROM pdu P1
+                               INNER JOIN pdu P3
+                               ON P1.m_id = P3.m_id AND P3.m_type = 136
+                               LEFT JOIN pdu P2
+                               ON P1.m_id = P2.m_id AND P2.m_type = 134) T
+                   ON (msg_id = id2 AND type = 151)
+                   OR (msg_id = id3 AND type = 137)
+                   WHERE T.id1 = ?;
+                 */
+                qb.setTables("addr INNER JOIN (SELECT P1._id AS id1, P2._id" +
+                             " AS id2, P3._id AS id3, ifnull(P2.st, 0) AS" +
+                             " delivery_status, ifnull(P3.read_status, 0) AS" +
+                             " read_status FROM pdu P1 INNER JOIN pdu P2 ON" +
+                             " P1.m_id=P2.m_id AND P2.m_type=134 LEFT JOIN" +
+                             " pdu P3 ON P1.m_id=P3.m_id AND P3.m_type=136" +
+                             " UNION SELECT P1._id AS id1, P2._id AS id2, P3._id" +
+                             " AS id3, ifnull(P2.st, 0) AS delivery_status," +
+                             " ifnull(P3.read_status, 0) AS read_status FROM" +
+                             " pdu P1 INNER JOIN pdu P3 ON P1.m_id=P3.m_id AND" +
+                             " P3.m_type=136 LEFT JOIN pdu P2 ON P1.m_id=P2.m_id" +
+                             " AND P2.m_type=134) T ON (msg_id=id2 AND type=151)" +
+                             " OR (msg_id=id3 AND type=137)");
+                qb.appendWhere("T.id1 = " + uri.getLastPathSegment());
+                qb.setDistinct(true);
+                break;
+            case MMS_REPORT_REQUEST:
+                /*
+                   SELECT address, d_rpt, rr
+                   FROM addr join pdu on pdu._id = addr.msg_id
+                   WHERE pdu._id = messageId AND addr.type = 151
+                 */
+                qb.setTables(TABLE_ADDR + " join " +
+                        TABLE_PDU + " on pdu._id = addr.msg_id");
+                qb.appendWhere("pdu._id = " + uri.getLastPathSegment());
+                qb.appendWhere(" AND " + "addr.type = " + PduHeaders.TO);
+                break;
+            case MMS_SENDING_RATE:
+                qb.setTables(TABLE_RATE);
+                break;
+            case MMS_DRM_STORAGE_ID:
+                qb.setTables(TABLE_DRM);
+                qb.appendWhere(BaseColumns._ID + "=" + uri.getLastPathSegment());
+                break;
+            default:
+                Log.e(TAG, "Invalid request: " + uri);
+                return null;
+        }
+
+        String finalSortOrder = null;
+        if (TextUtils.isEmpty(sortOrder)) {
+            if (qb.getTables().equals(TABLE_PDU)) {
+                finalSortOrder = Mms.DATE + " DESC";
+            } else if (qb.getTables().equals(TABLE_PART)) {
+                finalSortOrder = Part.SEQ;
+            }
+        } else {
+            finalSortOrder = sortOrder;
+        }
+
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor ret = qb.query(db, projection, selection,
+                selectionArgs, null, null, finalSortOrder);
+
+        // TODO: Does this need to be a URI for this provider.
+        ret.setNotificationUri(getContext().getContentResolver(), uri);
+        return ret;
+    }
+
+    private void constructQueryForBox(SQLiteQueryBuilder qb, int msgBox) {
+        qb.setTables(TABLE_PDU);
+
+        if (msgBox != Mms.MESSAGE_BOX_ALL) {
+            qb.appendWhere(Mms.MESSAGE_BOX + "=" + msgBox);
+        }
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        int match = sURLMatcher.match(uri);
+        switch (match) {
+            case MMS_ALL:
+            case MMS_INBOX:
+            case MMS_SENT:
+            case MMS_DRAFTS:
+            case MMS_OUTBOX:
+                return VND_ANDROID_DIR_MMS;
+            case MMS_ALL_ID:
+            case MMS_INBOX_ID:
+            case MMS_SENT_ID:
+            case MMS_DRAFTS_ID:
+            case MMS_OUTBOX_ID:
+                return VND_ANDROID_MMS;
+            case MMS_PART_ID: {
+                Cursor cursor = mOpenHelper.getReadableDatabase().query(
+                        TABLE_PART, new String[] { Part.CONTENT_TYPE },
+                        Part._ID + " = ?", new String[] { uri.getLastPathSegment() },
+                        null, null, null);
+                if (cursor != null) {
+                    try {
+                        if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
+                            return cursor.getString(0);
+                        } else {
+                            Log.e(TAG, "cursor.count() != 1: " + uri);
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                } else {
+                    Log.e(TAG, "cursor == null: " + uri);
+                }
+                return "*/*";
+            }
+            case MMS_ALL_PART:
+            case MMS_MSG_PART:
+            case MMS_MSG_ADDR:
+            default:
+                return "*/*";
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        int msgBox = Mms.MESSAGE_BOX_ALL;
+        boolean notify = true;
+
+        int match = sURLMatcher.match(uri);
+        if (LOCAL_LOGV) {
+            Log.v(TAG, "Insert uri=" + uri + ", match=" + match);
+        }
+
+        String table = TABLE_PDU;
+        switch (match) {
+            case MMS_ALL:
+                Object msgBoxObj = values.getAsInteger(Mms.MESSAGE_BOX);
+                if (msgBoxObj != null) {
+                    msgBox = (Integer) msgBoxObj;
+                }
+                else {
+                    // default to inbox
+                    msgBox = Mms.MESSAGE_BOX_INBOX;
+                }
+                break;
+            case MMS_INBOX:
+                msgBox = Mms.MESSAGE_BOX_INBOX;
+                break;
+            case MMS_SENT:
+                msgBox = Mms.MESSAGE_BOX_SENT;
+                break;
+            case MMS_DRAFTS:
+                msgBox = Mms.MESSAGE_BOX_DRAFTS;
+                break;
+            case MMS_OUTBOX:
+                msgBox = Mms.MESSAGE_BOX_OUTBOX;
+                break;
+            case MMS_MSG_PART:
+                notify = false;
+                table = TABLE_PART;
+                break;
+            case MMS_MSG_ADDR:
+                notify = false;
+                table = TABLE_ADDR;
+                break;
+            case MMS_SENDING_RATE:
+                notify = false;
+                table = TABLE_RATE;
+                break;
+            case MMS_DRM_STORAGE:
+                notify = false;
+                table = TABLE_DRM;
+                break;
+            default:
+                Log.e(TAG, "Invalid request: " + uri);
+                return null;
+        }
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        ContentValues finalValues;
+        Uri res = Mms.CONTENT_URI;
+        long rowId;
+
+        if (table.equals(TABLE_PDU)) {
+            boolean addDate = !values.containsKey(Mms.DATE);
+            boolean addMsgBox = !values.containsKey(Mms.MESSAGE_BOX);
+
+            // Filter keys we don't support yet.
+            filterUnsupportedKeys(values);
+
+            // TODO: Should initialValues be validated, e.g. if it
+            // missed some significant keys?
+            finalValues = new ContentValues(values);
+
+            long timeInMillis = System.currentTimeMillis();
+
+            if (addDate) {
+                finalValues.put(Mms.DATE, timeInMillis / 1000L);
+            }
+
+            if (addMsgBox && (msgBox != Mms.MESSAGE_BOX_ALL)) {
+                finalValues.put(Mms.MESSAGE_BOX, msgBox);
+            }
+
+            if (msgBox != Mms.MESSAGE_BOX_INBOX) {
+                // Mark all non-inbox messages read.
+                finalValues.put(Mms.READ, 1);
+            }
+
+            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
+                Log.e(TAG, "MmsProvider.insert: failed! " + finalValues);
+                return null;
+            }
+
+            res = Uri.parse(res + "/" + rowId);
+
+        } else if (table.equals(TABLE_ADDR)) {
+            finalValues = new ContentValues(values);
+            finalValues.put(Addr.MSG_ID, uri.getPathSegments().get(0));
+
+            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
+                Log.e(TAG, "Failed to insert address: " + finalValues);
+                return null;
+            }
+
+            res = Uri.parse(res + "/addr/" + rowId);
+        } else if (table.equals(TABLE_PART)) {
+            finalValues = new ContentValues(values);
+
+            if (match == MMS_MSG_PART) {
+                finalValues.put(Part.MSG_ID, uri.getPathSegments().get(0));
+            }
+
+            // Generate the '_data' field of the part with default
+            // permission settings.
+            String path = getContext().getDir("parts", 0).getPath()
+                    + "/PART_" + System.currentTimeMillis();
+
+            finalValues.put(Part._DATA, path);
+
+            File partFile = new File(path);
+            if (!partFile.exists()) {
+                try {
+                    if (!partFile.createNewFile()) {
+                        throw new IllegalStateException(
+                                "Unable to create new partFile: " + path);
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "createNewFile", e);
+                    throw new IllegalStateException(
+                            "Unable to create new partFile: " + path);
+                }
+            }
+
+            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
+                Log.e(TAG, "MmsProvider.insert: failed! " + finalValues);
+                return null;
+            }
+
+            res = Uri.parse(res + "/part/" + rowId);
+        } else if (table.equals(TABLE_RATE)) {
+            long now = values.getAsLong(Rate.SENT_TIME);
+            long oneHourAgo = now - 1000 * 60 * 60;
+            // Delete all unused rows (time earlier than one hour ago).
+            db.delete(table, Rate.SENT_TIME + "<=" + oneHourAgo, null);
+            db.insert(table, null, values);
+        } else if (table.equals(TABLE_DRM)) {
+            String path = getContext().getDir("parts", 0).getPath()
+                    + "/PART_" + System.currentTimeMillis();
+            finalValues = new ContentValues(1);
+            finalValues.put("_data", path);
+
+            File partFile = new File(path);
+            if (!partFile.exists()) {
+                try {
+                    if (!partFile.createNewFile()) {
+                        throw new IllegalStateException(
+                                "Unable to create new file: " + path);
+                    }
+                } catch (IOException e) {
+                    Log.e(TAG, "createNewFile", e);
+                    throw new IllegalStateException(
+                            "Unable to create new file: " + path);
+                }
+            }
+
+            if ((rowId = db.insert(table, null, finalValues)) <= 0) {
+                Log.e(TAG, "MmsProvider.insert: failed! " + finalValues);
+                return null;
+            }
+            res = Uri.parse(res + "/drm/" + rowId);
+        } else {
+            throw new AssertionError("Unknown table type: " + table);
+        }
+
+        if (notify) {
+            notifyChange();
+        }
+        return res;
+    }
+
+    private int getMessageBoxByMatch(int match) {
+        switch (match) {
+            case MMS_INBOX_ID:
+            case MMS_INBOX:
+                return Mms.MESSAGE_BOX_INBOX;
+            case MMS_SENT_ID:
+            case MMS_SENT:
+                return Mms.MESSAGE_BOX_SENT;
+            case MMS_DRAFTS_ID:
+            case MMS_DRAFTS:
+                return Mms.MESSAGE_BOX_DRAFTS;
+            case MMS_OUTBOX_ID:
+            case MMS_OUTBOX:
+                return Mms.MESSAGE_BOX_OUTBOX;
+            default:
+                throw new IllegalArgumentException("bad Arg: " + match);
+        }
+    }
+
+    @Override
+    public int delete(Uri uri, String selection,
+            String[] selectionArgs) {
+        int match = sURLMatcher.match(uri);
+        if (LOCAL_LOGV) {
+            Log.v(TAG, "Delete uri=" + uri + ", match=" + match);
+        }
+
+        String table, extraSelection = null;
+        boolean notify = false;
+
+        switch (match) {
+            case MMS_ALL_ID:
+            case MMS_INBOX_ID:
+            case MMS_SENT_ID:
+            case MMS_DRAFTS_ID:
+            case MMS_OUTBOX_ID:
+                notify = true;
+                table = TABLE_PDU;
+                extraSelection = Mms._ID + "=" + uri.getLastPathSegment();
+                break;
+            case MMS_ALL:
+            case MMS_INBOX:
+            case MMS_SENT:
+            case MMS_DRAFTS:
+            case MMS_OUTBOX:
+                notify = true;
+                table = TABLE_PDU;
+                if (match != MMS_ALL) {
+                    int msgBox = getMessageBoxByMatch(match);
+                    extraSelection = Mms.MESSAGE_BOX + "=" + msgBox;
+                }
+                break;
+            case MMS_ALL_PART:
+                table = TABLE_PART;
+                break;
+            case MMS_MSG_PART:
+                table = TABLE_PART;
+                extraSelection = Part.MSG_ID + "=" + uri.getPathSegments().get(0);
+                break;
+            case MMS_PART_ID:
+                table = TABLE_PART;
+                extraSelection = Part._ID + "=" + uri.getPathSegments().get(1);
+                break;
+            case MMS_MSG_ADDR:
+                table = TABLE_ADDR;
+                extraSelection = Addr.MSG_ID + "=" + uri.getPathSegments().get(0);
+                break;
+            case MMS_DRM_STORAGE:
+                table = TABLE_DRM;
+                break;
+            default:
+                Log.w(TAG, "No match for URI '" + uri + "'");
+                return 0;
+        }
+
+        String finalSelection = concatSelections(selection, extraSelection);
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int deletedRows = 0;
+
+        if (TABLE_PDU.equals(table)) {
+            deletedRows = deleteMessages(getContext(), db, finalSelection,
+                                         selectionArgs, uri);
+        } else if (TABLE_PART.equals(table)) {
+            deletedRows = deleteParts(db, finalSelection, selectionArgs);
+        } else if (TABLE_DRM.equals(table)) {
+            deletedRows = deleteTempDrmData(db, finalSelection, selectionArgs);
+        } else {
+            deletedRows = db.delete(table, finalSelection, selectionArgs);
+        }
+
+        if ((deletedRows > 0) && notify) {
+            notifyChange();
+        }
+        return deletedRows;
+    }
+
+    static int deleteMessages(Context context, SQLiteDatabase db,
+            String selection, String[] selectionArgs, Uri uri) {
+        Cursor cursor = db.query(TABLE_PDU, new String[] { Mms._ID },
+                selection, selectionArgs, null, null, null);
+        if (cursor == null) {
+            return 0;
+        }
+
+        try {
+            if (cursor.getCount() == 0) {
+                return 0;
+            }
+
+            while (cursor.moveToNext()) {
+                deleteParts(db, Part.MSG_ID + " = ?",
+                        new String[] { String.valueOf(cursor.getLong(0)) });
+            }
+        } finally {
+            cursor.close();
+        }
+
+        int count = db.delete(TABLE_PDU, selection, selectionArgs);
+        if (count > 0) {
+            Intent intent = new Intent(Mms.Intents.CONTENT_CHANGED_ACTION);
+            intent.putExtra(Mms.Intents.DELETED_CONTENTS, uri);
+            if (LOCAL_LOGV) {
+                Log.v(TAG, "Broadcasting intent: " + intent);
+            }
+            context.sendBroadcast(intent);
+        }
+        return count;
+    }
+
+    private static int deleteParts(SQLiteDatabase db, String selection,
+            String[] selectionArgs) {
+        return deleteDataRows(db, TABLE_PART, selection, selectionArgs);
+    }
+
+    private static int deleteTempDrmData(SQLiteDatabase db, String selection,
+            String[] selectionArgs) {
+        return deleteDataRows(db, TABLE_DRM, selection, selectionArgs);
+    }
+
+    private static int deleteDataRows(SQLiteDatabase db, String table,
+            String selection, String[] selectionArgs) {
+        Cursor cursor = db.query(table, new String[] { "_data" },
+                selection, selectionArgs, null, null, null);
+        if (cursor == null) {
+            // FIXME: This might be an error, ignore it may cause
+            // unpredictable result.
+            return 0;
+        }
+
+        try {
+            if (cursor.getCount() == 0) {
+                return 0;
+            }
+
+            while (cursor.moveToNext()) {
+                try {
+                    // Delete the associated files saved on file-system.
+                    new File(cursor.getString(0)).delete();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return db.delete(table, selection, selectionArgs);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values,
+            String selection, String[] selectionArgs) {
+        int match = sURLMatcher.match(uri);
+        if (LOCAL_LOGV) {
+            Log.v(TAG, "Update uri=" + uri + ", match=" + match);
+        }
+
+        boolean notify = false;
+        String msgId = null;
+        String table;
+
+        switch (match) {
+            case MMS_ALL_ID:
+            case MMS_INBOX_ID:
+            case MMS_SENT_ID:
+            case MMS_DRAFTS_ID:
+            case MMS_OUTBOX_ID:
+                msgId = uri.getLastPathSegment();
+            // fall-through
+            case MMS_ALL:
+            case MMS_INBOX:
+            case MMS_SENT:
+            case MMS_DRAFTS:
+            case MMS_OUTBOX:
+                notify = true;
+                table = TABLE_PDU;
+                break;
+            case MMS_MSG_PART:
+            case MMS_PART_ID:
+                table = TABLE_PART;
+                break;
+            default:
+                Log.w(TAG, "Update operation for '" + uri + "' not implemented.");
+                return 0;
+        }
+
+        String extraSelection = null;
+        ContentValues finalValues;
+        if (table.equals(TABLE_PDU)) {
+            // Filter keys that we don't support yet.
+            filterUnsupportedKeys(values);
+            finalValues = new ContentValues(values);
+
+            if (msgId != null) {
+                extraSelection = Mms._ID + "=" + msgId;
+            }
+        } else if (table.equals(TABLE_PART)) {
+            finalValues = new ContentValues(values);
+
+            switch (match) {
+                case MMS_MSG_PART:
+                    extraSelection = Part.MSG_ID + "=" + uri.getPathSegments().get(0);
+                    break;
+                case MMS_PART_ID:
+                    extraSelection = Part._ID + "=" + uri.getPathSegments().get(1);
+                    break;
+                default:
+                    break;
+            }
+        } else {
+            return 0;
+        }
+
+        String finalSelection = concatSelections(selection, extraSelection);
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int count = db.update(table, finalValues, finalSelection, selectionArgs);
+        if (notify && (count > 0)) {
+            notifyChange();
+        }
+        return count;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode)
+            throws FileNotFoundException {
+        return openFileHelper(uri, mode);
+    }
+
+    private void filterUnsupportedKeys(ContentValues values) {
+        // Some columns are unsupported.  They should therefore
+        // neither be inserted nor updated.  Filter them out.
+        values.remove(Mms.DELIVERY_TIME_TOKEN);
+        values.remove(Mms.SENDER_VISIBILITY);
+        values.remove(Mms.REPLY_CHARGING);
+        values.remove(Mms.REPLY_CHARGING_DEADLINE_TOKEN);
+        values.remove(Mms.REPLY_CHARGING_DEADLINE);
+        values.remove(Mms.REPLY_CHARGING_ID);
+        values.remove(Mms.REPLY_CHARGING_SIZE);
+        values.remove(Mms.PREVIOUSLY_SENT_BY);
+        values.remove(Mms.PREVIOUSLY_SENT_DATE);
+        values.remove(Mms.STORE);
+        values.remove(Mms.MM_STATE);
+        values.remove(Mms.MM_FLAGS_TOKEN);
+        values.remove(Mms.MM_FLAGS);
+        values.remove(Mms.STORE_STATUS);
+        values.remove(Mms.STORE_STATUS_TEXT);
+        values.remove(Mms.STORED);
+        values.remove(Mms.TOTALS);
+        values.remove(Mms.MBOX_TOTALS);
+        values.remove(Mms.MBOX_TOTALS_TOKEN);
+        values.remove(Mms.QUOTAS);
+        values.remove(Mms.MBOX_QUOTAS);
+        values.remove(Mms.MBOX_QUOTAS_TOKEN);
+        values.remove(Mms.MESSAGE_COUNT);
+        values.remove(Mms.START);
+        values.remove(Mms.DISTRIBUTION_INDICATOR);
+        values.remove(Mms.ELEMENT_DESCRIPTOR);
+        values.remove(Mms.LIMIT);
+        values.remove(Mms.RECOMMENDED_RETRIEVAL_MODE);
+        values.remove(Mms.RECOMMENDED_RETRIEVAL_MODE_TEXT);
+        values.remove(Mms.STATUS_TEXT);
+        values.remove(Mms.APPLIC_ID);
+        values.remove(Mms.REPLY_APPLIC_ID);
+        values.remove(Mms.AUX_APPLIC_ID);
+        values.remove(Mms.DRM_CONTENT);
+        values.remove(Mms.ADAPTATION_ALLOWED);
+        values.remove(Mms.REPLACE_ID);
+        values.remove(Mms.CANCEL_ID);
+        values.remove(Mms.CANCEL_STATUS);
+
+        // Keys shouldn't be inserted or updated.
+        values.remove(Mms._ID);
+    }
+
+    private void notifyChange() {
+        getContext().getContentResolver().notifyChange(
+                MmsSms.CONTENT_URI, null);
+    }
+
+    private final static String TAG = "MmsProvider";
+    private final static String VND_ANDROID_MMS = "vnd.android/mms";
+    private final static String VND_ANDROID_DIR_MMS = "vnd.android-dir/mms";
+    private final static boolean DEBUG = false;
+    private final static boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+    private static final int MMS_ALL                      = 0;
+    private static final int MMS_ALL_ID                   = 1;
+    private static final int MMS_INBOX                    = 2;
+    private static final int MMS_INBOX_ID                 = 3;
+    private static final int MMS_SENT                     = 4;
+    private static final int MMS_SENT_ID                  = 5;
+    private static final int MMS_DRAFTS                   = 6;
+    private static final int MMS_DRAFTS_ID                = 7;
+    private static final int MMS_OUTBOX                   = 8;
+    private static final int MMS_OUTBOX_ID                = 9;
+    private static final int MMS_ALL_PART                 = 10;
+    private static final int MMS_MSG_PART                 = 11;
+    private static final int MMS_PART_ID                  = 12;
+    private static final int MMS_MSG_ADDR                 = 13;
+    private static final int MMS_SENDING_RATE             = 14;
+    private static final int MMS_REPORT_STATUS          = 15;
+    private static final int MMS_REPORT_REQUEST = 16;
+    private static final int MMS_DRM_STORAGE              = 17;
+    private static final int MMS_DRM_STORAGE_ID           = 18;
+
+    private static final UriMatcher
+            sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    static {
+        sURLMatcher.addURI("mms", null,         MMS_ALL);
+        sURLMatcher.addURI("mms", "#",          MMS_ALL_ID);
+        sURLMatcher.addURI("mms", "inbox",      MMS_INBOX);
+        sURLMatcher.addURI("mms", "inbox/#",    MMS_INBOX_ID);
+        sURLMatcher.addURI("mms", "sent",       MMS_SENT);
+        sURLMatcher.addURI("mms", "sent/#",     MMS_SENT_ID);
+        sURLMatcher.addURI("mms", "drafts",     MMS_DRAFTS);
+        sURLMatcher.addURI("mms", "drafts/#",   MMS_DRAFTS_ID);
+        sURLMatcher.addURI("mms", "outbox",     MMS_OUTBOX);
+        sURLMatcher.addURI("mms", "outbox/#",   MMS_OUTBOX_ID);
+        sURLMatcher.addURI("mms", "part",       MMS_ALL_PART);
+        sURLMatcher.addURI("mms", "#/part",     MMS_MSG_PART);
+        sURLMatcher.addURI("mms", "part/#",     MMS_PART_ID);
+        sURLMatcher.addURI("mms", "#/addr",     MMS_MSG_ADDR);
+        sURLMatcher.addURI("mms", "rate",       MMS_SENDING_RATE);
+        sURLMatcher.addURI("mms", "report-status/#",  MMS_REPORT_STATUS);
+        sURLMatcher.addURI("mms", "report-request/#", MMS_REPORT_REQUEST);
+        sURLMatcher.addURI("mms", "drm",        MMS_DRM_STORAGE);
+        sURLMatcher.addURI("mms", "drm/#",      MMS_DRM_STORAGE_ID);
+    }
+
+    private SQLiteOpenHelper mOpenHelper;
+
+    private static String concatSelections(String selection1, String selection2) {
+        if (TextUtils.isEmpty(selection1)) {
+            return selection2;
+        } else if (TextUtils.isEmpty(selection2)) {
+            return selection1;
+        } else {
+            return selection1 + " AND " + selection2;
+        }
+    }
+}
+
diff --git a/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
new file mode 100644
index 0000000..31d7cb7
--- /dev/null
+++ b/src/com/android/providers/telephony/MmsSmsDatabaseHelper.java
@@ -0,0 +1,2035 @@
+/*
+ * Copyright (C) 2008 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 static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_DELIVERY_IND;
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND;
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_ORIG_IND;
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_READ_REC_IND;
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF;
+import static com.google.android.mms.pdu.PduHeaders.MESSAGE_TYPE_SEND_REQ;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.BaseColumns;
+import android.provider.Telephony.Mms;
+import android.provider.Telephony.MmsSms;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.Threads;
+import android.provider.Telephony.Mms.Addr;
+import android.provider.Telephony.Mms.Part;
+import android.provider.Telephony.Mms.Rate;
+import android.provider.Telephony.MmsSms.PendingMessages;
+import android.util.Log;
+
+public class MmsSmsDatabaseHelper extends SQLiteOpenHelper {
+    private static final String TAG = "MmsSmsDatabaseHelper";
+
+    private static final String SMS_UPDATE_THREAD_READ_BODY =
+                        "  UPDATE threads SET read = " +
+                        "    CASE (SELECT COUNT(*)" +
+                        "          FROM sms" +
+                        "          WHERE " + Sms.READ + " = 0" +
+                        "            AND " + Sms.THREAD_ID + " = threads._id)" +
+                        "      WHEN 0 THEN 1" +
+                        "      ELSE 0" +
+                        "    END" +
+                        "  WHERE threads._id = new." + Sms.THREAD_ID + "; ";
+
+    private static final String UPDATE_THREAD_COUNT_ON_NEW =
+                        "  UPDATE threads SET message_count = " +
+                        "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                        "      ON threads._id = " + Sms.THREAD_ID +
+                        "      WHERE " + Sms.THREAD_ID + " = new.thread_id" +
+                        "        AND sms." + Sms.TYPE + " != 3) + " +
+                        "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                        "      ON threads._id = " + Mms.THREAD_ID +
+                        "      WHERE " + Mms.THREAD_ID + " = new.thread_id" +
+                        "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                        "        AND " + Mms.MESSAGE_BOX + " != 3) " +
+                        "  WHERE threads._id = new.thread_id; ";
+
+    private static final String UPDATE_THREAD_COUNT_ON_OLD =
+                        "  UPDATE threads SET message_count = " +
+                        "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                        "      ON threads._id = " + Sms.THREAD_ID +
+                        "      WHERE " + Sms.THREAD_ID + " = old.thread_id" +
+                        "        AND sms." + Sms.TYPE + " != 3) + " +
+                        "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                        "      ON threads._id = " + Mms.THREAD_ID +
+                        "      WHERE " + Mms.THREAD_ID + " = old.thread_id" +
+                        "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                        "        AND " + Mms.MESSAGE_BOX + " != 3) " +
+                        "  WHERE threads._id = old.thread_id; ";
+
+    private static final String SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
+                        "BEGIN" +
+                        "  UPDATE threads SET" +
+                        "    date = (strftime('%s','now') * 1000), " +
+                        "    snippet = new." + Sms.BODY + ", " +
+                        "    snippet_cs = 0" +
+                        "  WHERE threads._id = new." + Sms.THREAD_ID + "; " +
+                        UPDATE_THREAD_COUNT_ON_NEW +
+                        SMS_UPDATE_THREAD_READ_BODY +
+                        "END;";
+
+    private static final String PDU_UPDATE_THREAD_CONSTRAINTS =
+                        "  WHEN new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_RETRIEVE_CONF +
+                        "    OR new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND +
+                        "    OR new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_SEND_REQ + " ";
+
+    private static final String PDU_UPDATE_THREAD_READ_BODY =
+                        "  UPDATE threads SET read = " +
+                        "    CASE (SELECT COUNT(*)" +
+                        "          FROM " + MmsProvider.TABLE_PDU +
+                        "          WHERE " + Mms.READ + " = 0" +
+                        "            AND " + Mms.THREAD_ID + " = threads._id)" +
+                        "      WHEN 0 THEN 1" +
+                        "      ELSE 0" +
+                        "    END" +
+                        "  WHERE threads._id = new." + Mms.THREAD_ID + "; ";
+
+    private static final String PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE =
+                        "BEGIN" +
+                        "  UPDATE threads SET" +
+                        "    date = (strftime('%s','now') * 1000), " +
+                        "    snippet = new." + Mms.SUBJECT + ", " +
+                        "    snippet_cs = new." + Mms.SUBJECT_CHARSET +
+                        "  WHERE threads._id = new." + Mms.THREAD_ID + "; " +
+                        UPDATE_THREAD_COUNT_ON_NEW +
+                        PDU_UPDATE_THREAD_READ_BODY +
+                        "END;";
+
+    private static final String UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE =
+                        "  UPDATE threads SET snippet = " +
+                        "   (SELECT snippet FROM" +
+                        "     (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
+                        "      UNION SELECT date, body AS snippet, thread_id FROM sms)" +
+                        "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
+                        "  WHERE threads._id = OLD.thread_id; " +
+                        "  UPDATE threads SET snippet_cs = " +
+                        "   (SELECT snippet_cs FROM" +
+                        "     (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
+                        "      UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
+                        "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
+                        "  WHERE threads._id = OLD.thread_id; ";
+
+    private static MmsSmsDatabaseHelper mInstance = null;
+
+    static final String DATABASE_NAME = "mmssms.db";
+    static final int DATABASE_VERSION = 41;
+
+    private MmsSmsDatabaseHelper(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+    }
+
+    /**
+     * Return a singleton helper for the combined MMS and SMS
+     * database.
+     */
+    /* package */ static synchronized MmsSmsDatabaseHelper getInstance(Context context) {
+        if (mInstance == null) {
+            mInstance = new MmsSmsDatabaseHelper(context);
+        }
+        return mInstance;
+    }
+
+    public static void updateThread(SQLiteDatabase db, long thread_id) {
+        if (thread_id < 0) {
+            updateAllThreads(db, null, null);
+            return;
+        }
+        
+        // Delete the row for this thread in the threads table if
+        // there are no more messages attached to it in either
+        // the sms or pdu tables.
+        int rows = db.delete("threads",
+                  "_id = ? AND _id NOT IN" +
+                  "          (SELECT thread_id FROM sms " +
+                  "           UNION SELECT thread_id FROM pdu)",
+                  new String[] { String.valueOf(thread_id) });
+        if (rows > 0) {
+            // If this deleted a row, we have no more work to do.
+            return;
+        }
+        // Update the message count in the threads table as the sum
+        // of all messages in both the sms and pdu tables.
+        db.execSQL(
+            "  UPDATE threads SET message_count = " +
+            "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+            "      ON threads._id = " + Sms.THREAD_ID +
+            "      WHERE " + Sms.THREAD_ID + " = " + thread_id +
+            "        AND sms." + Sms.TYPE + " != 3) + " +
+            "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+            "      ON threads._id = " + Mms.THREAD_ID +
+            "      WHERE " + Mms.THREAD_ID + " = " + thread_id +
+            "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+            "        AND " + Mms.MESSAGE_BOX + " != 3) " +
+            "  WHERE threads._id = " + thread_id + ";");
+
+        // Update the date and the snippet (and its character set) in
+        // the threads table to be that of the most recent message in
+        // the thread.
+        db.execSQL(
+            "  UPDATE threads" + 
+            "  SET" + 
+            "  date =" + 
+            "    (SELECT date FROM" + 
+            "        (SELECT date * 1000 AS date, thread_id FROM pdu" + 
+            "         UNION SELECT date, thread_id FROM sms)" + 
+            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," + 
+            "  snippet =" + 
+            "    (SELECT snippet FROM" + 
+            "        (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" + 
+            "         UNION SELECT date, body AS snippet, thread_id FROM sms)" + 
+            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)," + 
+            "  snippet_cs =" + 
+            "    (SELECT snippet_cs FROM" + 
+            "        (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" + 
+            "         UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" + 
+            "     WHERE thread_id = " + thread_id + " ORDER BY date DESC LIMIT 1)" + 
+            "  WHERE threads._id = " + thread_id + ";"); 
+
+        // Update the error column of the thread to indicate if there
+        // are any messages in it that have failed to send.
+        db.execSQL(
+            "UPDATE threads SET error =" +
+            "       (SELECT COUNT(*) FROM sms WHERE type=5" +
+            "        AND thread_id = " + thread_id + " LIMIT 1)" +
+            "   WHERE threads._id = " + thread_id + ";");
+    }
+    
+    public static void updateAllThreads(SQLiteDatabase db, String where, String[] whereArgs) {
+        if (where == null) {
+            where = "";
+        } else {
+            where = "WHERE (" + where + ")";
+        }
+        String query = "SELECT _id FROM threads WHERE _id IN " +
+                       "(SELECT DISTINCT thread_id FROM sms " + where + ")";
+        Cursor c = db.rawQuery(query, whereArgs);
+        if (c != null) {
+            while (c.moveToNext()) {
+                updateThread(db, c.getInt(0));
+            }
+            c.close();
+        }
+        // remove orphaned threads
+        db.delete("threads",
+                "_id NOT IN (SELECT DISTINCT thread_id FROM sms " +
+                "UNION SELECT DISTINCT thread_id FROM pdu)", null);
+    }
+    
+    public static int deleteOneSms(SQLiteDatabase db, int message_id) {
+        int thread_id = -1;
+        // Find the thread ID that the specified SMS belongs to.
+        Cursor c = db.query("sms", new String[] { "thread_id" },
+                            "_id=" + message_id, null, null, null, null);
+        if (c != null) {
+            if (c.moveToFirst()) {
+                thread_id = c.getInt(0);
+            }
+            c.close();
+        }
+
+        // Delete the specified message.
+        int rows = db.delete("sms", "_id=" + message_id, null);
+        if (thread_id > 0) {
+            // Update its thread.
+            updateThread(db, thread_id);
+        }
+        return rows;
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        createMmsTables(db);
+        createSmsTables(db);
+        createCommonTables(db);
+        createCommonTriggers(db);
+        createMmsTriggers(db);
+    }
+
+    private void createMmsTables(SQLiteDatabase db) {
+        // N.B.: Whenever the columns here are changed, the columns in
+        // {@ref MmsSmsProvider} must be changed to match.
+        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PDU + " (" +
+                   Mms._ID + " INTEGER PRIMARY KEY," +
+                   Mms.THREAD_ID + " INTEGER," +
+                   Mms.DATE + " INTEGER," +
+                   Mms.MESSAGE_BOX + " INTEGER," +
+                   Mms.READ + " INTEGER DEFAULT 0," +
+                   Mms.MESSAGE_ID + " TEXT," +
+                   Mms.SUBJECT + " TEXT," +
+                   Mms.SUBJECT_CHARSET + " INTEGER," +
+                   Mms.CONTENT_TYPE + " TEXT," +
+                   Mms.CONTENT_LOCATION + " TEXT," +
+                   Mms.EXPIRY + " INTEGER," +
+                   Mms.MESSAGE_CLASS + " TEXT," +
+                   Mms.MESSAGE_TYPE + " INTEGER," +
+                   Mms.MMS_VERSION + " INTEGER," +
+                   Mms.MESSAGE_SIZE + " INTEGER," +
+                   Mms.PRIORITY + " INTEGER," +
+                   Mms.READ_REPORT + " INTEGER," +
+                   Mms.REPORT_ALLOWED + " INTEGER," +
+                   Mms.RESPONSE_STATUS + " INTEGER," +
+                   Mms.STATUS + " INTEGER," +
+                   Mms.TRANSACTION_ID + " TEXT," +
+                   Mms.RETRIEVE_STATUS + " INTEGER," +
+                   Mms.RETRIEVE_TEXT + " TEXT," +
+                   Mms.RETRIEVE_TEXT_CHARSET + " INTEGER," +
+                   Mms.READ_STATUS + " INTEGER," +
+                   Mms.CONTENT_CLASS + " INTEGER," +
+                   Mms.RESPONSE_TEXT + " TEXT," +
+                   Mms.DELIVERY_TIME + " INTEGER," +
+                   Mms.DELIVERY_REPORT + " INTEGER);");
+
+        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_ADDR + " (" +
+                   Addr._ID + " INTEGER PRIMARY KEY," +
+                   Addr.MSG_ID + " INTEGER," +
+                   Addr.CONTACT_ID + " INTEGER," +
+                   Addr.ADDRESS + " TEXT," +
+                   Addr.TYPE + " INTEGER," +
+                   Addr.CHARSET + " INTEGER);");
+
+        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_PART + " (" +
+                   Part._ID + " INTEGER PRIMARY KEY," +
+                   Part.MSG_ID + " INTEGER," +
+                   Part.SEQ + " INTEGER DEFAULT 0," +
+                   Part.CONTENT_TYPE + " TEXT," +
+                   Part.NAME + " TEXT," +
+                   Part.CHARSET + " INTEGER," +
+                   Part.CONTENT_DISPOSITION + " TEXT," +
+                   Part.FILENAME + " TEXT," +
+                   Part.CONTENT_ID + " TEXT," +
+                   Part.CONTENT_LOCATION + " TEXT," +
+                   Part.CT_START + " INTEGER," +
+                   Part.CT_TYPE + " TEXT," +
+                   Part._DATA + " TEXT);");
+
+        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_RATE + " (" +
+                   Rate.SENT_TIME + " INTEGER);");
+
+        db.execSQL("CREATE TABLE " + MmsProvider.TABLE_DRM + " (" +
+                   BaseColumns._ID + " INTEGER PRIMARY KEY," +
+                   "_data TEXT);");
+    }
+
+    private void createMmsTriggers(SQLiteDatabase db) {
+        // Cleans up parts when a MM is deleted.
+        db.execSQL("CREATE TRIGGER part_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
+                   "BEGIN " +
+                   "  DELETE FROM " + MmsProvider.TABLE_PART +
+                   "  WHERE " + Part.MSG_ID + "=old._id;" +
+                   "END;");
+
+        // Cleans up address info when a MM is deleted.
+        db.execSQL("CREATE TRIGGER addr_cleanup DELETE ON " + MmsProvider.TABLE_PDU + " " +
+                   "BEGIN " +
+                   "  DELETE FROM " + MmsProvider.TABLE_ADDR +
+                   "  WHERE " + Addr.MSG_ID + "=old._id;" +
+                   "END;");
+
+        // Delete obsolete delivery-report, read-report while deleting their
+        // associated Send.req.
+        db.execSQL("CREATE TRIGGER cleanup_delivery_and_read_report " +
+                   "AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
+                   "WHEN old." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_SEND_REQ + " " +
+                   "BEGIN " +
+                   "  DELETE FROM " + MmsProvider.TABLE_PDU +
+                   "  WHERE (" + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_DELIVERY_IND +
+                   "    OR " + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_READ_ORIG_IND + ")" +
+                   "    AND " + Mms.MESSAGE_ID + "=old." + Mms.MESSAGE_ID + "; " +
+                   "END;");
+    }
+
+    private void createSmsTables(SQLiteDatabase db) {
+        // N.B.: Whenever the columns here are changed, the columns in
+        // {@ref MmsSmsProvider} must be changed to match.
+        db.execSQL("CREATE TABLE sms (" +
+                   "_id INTEGER PRIMARY KEY," +
+                   "thread_id INTEGER," +
+                   "address TEXT," +
+                   "person INTEGER," +
+                   "date INTEGER," +
+                   "protocol INTEGER," +
+                   "read INTEGER DEFAULT 0," +
+                   "status INTEGER DEFAULT -1," + // a TP-Status value
+                                                  // or -1 if it
+                                                  // status hasn't
+                                                  // been received
+                   "type INTEGER," +
+                   "reply_path_present INTEGER," +
+                   "subject TEXT," +
+                   "body TEXT," +
+                   "service_center TEXT);");
+
+        /**
+         * This table is used by the SMS dispatcher to hold
+         * incomplete partial messages until all the parts arrive.
+         */
+        db.execSQL("CREATE TABLE raw (" +
+                   "_id INTEGER PRIMARY KEY," +
+                   "date INTEGER," +
+                   "reference_number INTEGER," + // one per full message
+                   "count INTEGER," + // the number of parts
+                   "sequence INTEGER," + // the part number of this message
+                   "destination_port INTEGER," +
+                   "address TEXT," +
+                   "pdu TEXT);"); // the raw PDU for this part
+
+        db.execSQL("CREATE TABLE attachments (" +
+                   "sms_id INTEGER," +
+                   "content_url TEXT," +
+                   "offset INTEGER);");
+
+        /**
+         * This table is used by the SMS dispatcher to hold pending
+         * delivery status report intents.
+         */
+        db.execSQL("CREATE TABLE sr_pending (" +
+                   "reference_number INTEGER," +
+                   "action TEXT," +
+                   "data TEXT);");
+    }
+
+    private void createCommonTables(SQLiteDatabase db) {
+        // TODO Ensure that each entry is removed when the last use of
+        // any address equivalent to its address is removed.
+
+        /**
+         * This table maps the first instance seen of any particular
+         * MMS/SMS address to an ID, which is then used as its
+         * canonical representation.  If the same address or an
+         * equivalent address (as determined by our Sqlite
+         * PHONE_NUMBERS_EQUAL extension) is seen later, this same ID
+         * will be used.
+         */
+        db.execSQL("CREATE TABLE canonical_addresses (" +
+                   "_id INTEGER PRIMARY KEY," +
+                   "address TEXT);");
+
+        /**
+         * This table maps the subject and an ordered set of recipient
+         * IDs, separated by spaces, to a unique thread ID.  The IDs
+         * come from the canonical_addresses table.  This works
+         * because messages are considered to be part of the same
+         * thread if they have the same subject (or a null subject)
+         * and the same set of recipients.
+         */
+        db.execSQL("CREATE TABLE threads (" +
+                   Threads._ID + " INTEGER PRIMARY KEY," +
+                   Threads.DATE + " INTEGER DEFAULT 0," +
+                   Threads.MESSAGE_COUNT + " INTEGER DEFAULT 0," +
+                   Threads.RECIPIENT_IDS + " TEXT," +
+                   Threads.SNIPPET + " TEXT," +
+                   Threads.SNIPPET_CHARSET + " INTEGER DEFAULT 0," +
+                   Threads.READ + " INTEGER DEFAULT 1," +
+                   Threads.TYPE + " INTEGER DEFAULT 0," +
+                   Threads.ERROR + " INTEGER DEFAULT 0);");
+
+        /**
+         * This table stores the queue of messages to be sent/downloaded.
+         */
+        db.execSQL("CREATE TABLE " + MmsSmsProvider.TABLE_PENDING_MSG +" (" +
+                   PendingMessages._ID + " INTEGER PRIMARY KEY," +
+                   PendingMessages.PROTO_TYPE + " INTEGER," +
+                   PendingMessages.MSG_ID + " INTEGER," +
+                   PendingMessages.MSG_TYPE + " INTEGER," +
+                   PendingMessages.ERROR_TYPE + " INTEGER," +
+                   PendingMessages.ERROR_CODE + " INTEGER," +
+                   PendingMessages.RETRY_INDEX + " INTEGER NOT NULL DEFAULT 0," +
+                   PendingMessages.DUE_TIME + " INTEGER," +
+                   PendingMessages.LAST_TRY + " INTEGER);");
+
+    }
+
+    // TODO Check the query plans for these triggers.
+    private void createCommonTriggers(SQLiteDatabase db) {
+        // Updates threads table whenever a message is added to pdu.
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON " +
+                   MmsProvider.TABLE_PDU + " " +
+                   PDU_UPDATE_THREAD_CONSTRAINTS +
+                   PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
+
+        // Updates threads table whenever a message is added to sms.
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
+                   SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
+
+        // Updates threads table whenever a message in pdu is updated.
+        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
+                   "  UPDATE OF " + Mms.DATE + ", " + Mms.SUBJECT + ", " + Mms.MESSAGE_BOX +
+                   "  ON " + MmsProvider.TABLE_PDU + " " +
+                   PDU_UPDATE_THREAD_CONSTRAINTS +
+                   PDU_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
+
+        // Updates threads table whenever a message in sms is updated.
+        db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
+                   "  UPDATE OF " + Sms.DATE + ", " + Sms.BODY + ", " + Sms.TYPE +
+                   "  ON sms " +
+                   SMS_UPDATE_THREAD_DATE_SNIPPET_COUNT_ON_UPDATE);
+
+        // Updates threads table whenever a message in pdu is updated.
+        db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
+                   "  UPDATE OF " + Mms.READ +
+                   "  ON " + MmsProvider.TABLE_PDU + " " +
+                   PDU_UPDATE_THREAD_CONSTRAINTS +
+                   "BEGIN " +
+                   PDU_UPDATE_THREAD_READ_BODY +
+                   "END;");
+
+        // Updates threads table whenever a message in sms is updated.
+        db.execSQL("CREATE TRIGGER sms_update_thread_read_on_update AFTER" +
+                   "  UPDATE OF " + Sms.READ +
+                   "  ON sms " +
+                   "BEGIN " +
+                   SMS_UPDATE_THREAD_READ_BODY +
+                   "END;");
+
+        // Update threads table whenever a message in pdu is deleted
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_delete " +
+                   "AFTER DELETE ON pdu " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000)" +
+                   "  WHERE threads._id = old." + Mms.THREAD_ID + "; " +
+                   UPDATE_THREAD_COUNT_ON_OLD +
+                   UPDATE_THREAD_SNIPPET_SNIPPET_CS_ON_DELETE +
+                   "END;");
+
+        // When the last message in a thread is deleted, these
+        // triggers ensure that the entry for its thread ID is removed
+        // from the threads table.
+        db.execSQL("CREATE TRIGGER delete_obsolete_threads_pdu " +
+                   "AFTER DELETE ON pdu " +
+                   "BEGIN " +
+                   "  DELETE FROM threads " +
+                   "  WHERE " +
+                   "    _id = old.thread_id " +
+                   "    AND _id NOT IN " +
+                   "    (SELECT thread_id FROM sms " +
+                   "     UNION SELECT thread_id from pdu); " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER delete_obsolete_threads_when_update_pdu " +
+                   "AFTER UPDATE OF " + Mms.THREAD_ID + " ON pdu " +
+                   "WHEN old." + Mms.THREAD_ID + " != new." + Mms.THREAD_ID + " " +
+                   "BEGIN " +
+                   "  DELETE FROM threads " +
+                   "  WHERE " +
+                   "    _id = old.thread_id " +
+                   "    AND _id NOT IN " +
+                   "    (SELECT thread_id FROM sms " +
+                   "     UNION SELECT thread_id from pdu); " +
+                   "END;");
+        // Insert pending status for M-Notification.ind or M-ReadRec.ind
+        // when they are inserted into Inbox/Outbox.
+        db.execSQL("CREATE TRIGGER insert_mms_pending_on_insert " +
+                   "AFTER INSERT ON pdu " +
+                   "WHEN new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_NOTIFICATION_IND +
+                   "  OR new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_READ_REC_IND + " " +
+                   "BEGIN " +
+                   "  INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
+                   "    (" + PendingMessages.PROTO_TYPE + "," +
+                   "     " + PendingMessages.MSG_ID + "," +
+                   "     " + PendingMessages.MSG_TYPE + "," +
+                   "     " + PendingMessages.ERROR_TYPE + "," +
+                   "     " + PendingMessages.ERROR_CODE + "," +
+                   "     " + PendingMessages.RETRY_INDEX + "," +
+                   "     " + PendingMessages.DUE_TIME + ") " +
+                   "  VALUES " +
+                   "    (" + MmsSms.MMS_PROTO + "," +
+                   "      new." + BaseColumns._ID + "," +
+                   "      new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
+                   "END;");
+
+        // Insert pending status for M-Send.req when it is moved into Outbox.
+        db.execSQL("CREATE TRIGGER insert_mms_pending_on_update " +
+                   "AFTER UPDATE ON pdu " +
+                   "WHEN new." + Mms.MESSAGE_TYPE + "=" + MESSAGE_TYPE_SEND_REQ +
+                   "  AND new." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
+                   "  AND old." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
+                   "BEGIN " +
+                   "  INSERT INTO " + MmsSmsProvider.TABLE_PENDING_MSG +
+                   "    (" + PendingMessages.PROTO_TYPE + "," +
+                   "     " + PendingMessages.MSG_ID + "," +
+                   "     " + PendingMessages.MSG_TYPE + "," +
+                   "     " + PendingMessages.ERROR_TYPE + "," +
+                   "     " + PendingMessages.ERROR_CODE + "," +
+                   "     " + PendingMessages.RETRY_INDEX + "," +
+                   "     " + PendingMessages.DUE_TIME + ") " +
+                   "  VALUES " +
+                   "    (" + MmsSms.MMS_PROTO + "," +
+                   "      new." + BaseColumns._ID + "," +
+                   "      new." + Mms.MESSAGE_TYPE + ",0,0,0,0);" +
+                   "END;");
+
+        // When a message is moved out of Outbox, delete its pending status.
+        db.execSQL("CREATE TRIGGER delete_mms_pending_on_update " +
+                   "AFTER UPDATE ON " + MmsProvider.TABLE_PDU + " " +
+                   "WHEN old." + Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_OUTBOX +
+                   "  AND new." + Mms.MESSAGE_BOX + "!=" + Mms.MESSAGE_BOX_OUTBOX + " " +
+                   "BEGIN " +
+                   "  DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
+                   "  WHERE " + PendingMessages.MSG_ID + "=new._id; " +
+                   "END;");
+
+        // Delete pending status for a message when it is deleted.
+        db.execSQL("CREATE TRIGGER delete_mms_pending_on_delete " +
+                   "AFTER DELETE ON " + MmsProvider.TABLE_PDU + " " +
+                   "BEGIN " +
+                   "  DELETE FROM " + MmsSmsProvider.TABLE_PENDING_MSG +
+                   "  WHERE " + PendingMessages.MSG_ID + "=old._id; " +
+                   "END;");
+
+        // TODO Add triggers for SMS retry-status management.
+
+        // Update the error flag of threads when the error type of
+        // a pending MM is updated.
+        db.execSQL("CREATE TRIGGER update_threads_error_on_update_mms " +
+                   "  AFTER UPDATE OF err_type ON pending_msgs " +
+                   "  WHEN (OLD.err_type < 10 AND NEW.err_type >= 10)" +
+                   "    OR (OLD.err_type >= 10 AND NEW.err_type < 10) " +
+                   "BEGIN" +
+                   "  UPDATE threads SET error = " +
+                   "    CASE" +
+                   "      WHEN NEW.err_type >= 10 THEN error + 1" +
+                   "      ELSE error - 1" +
+                   "    END " +
+                   "  WHERE _id =" +
+                   "   (SELECT DISTINCT thread_id" +
+                   "    FROM pdu" +
+                   "    WHERE _id = NEW.msg_id); " +
+                   "END;");
+
+        // Update the error flag of threads when delete pending message.
+        db.execSQL("CREATE TRIGGER update_threads_error_on_delete_mms " +
+                   "  BEFORE DELETE ON pdu" +
+                   "  WHEN OLD._id IN (SELECT DISTINCT msg_id" +
+                   "                   FROM pending_msgs" +
+                   "                   WHERE err_type >= 10) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET error = error - 1" +
+                   "  WHERE _id = OLD.thread_id; " +
+                   "END;");
+
+        // Update the error flag of threads while moving an MM out of Outbox,
+        // which was failed to be sent permanently.
+        db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
+                   "  BEFORE UPDATE OF msg_box ON pdu " +
+                   "  WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
+                   "  AND (OLD._id IN (SELECT DISTINCT msg_id" +
+                   "                   FROM pending_msgs" +
+                   "                   WHERE err_type >= 10)) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET error = error - 1" +
+                   "  WHERE _id = OLD.thread_id; " +
+                   "END;");
+
+        // Update the error flag of threads after a text message was
+        // failed to send/receive.
+        db.execSQL("CREATE TRIGGER update_threads_error_on_update_sms " +
+                   "  AFTER UPDATE OF type ON sms" +
+                   "  WHEN (OLD.type != 5 AND NEW.type = 5)" +
+                   "    OR (OLD.type = 5 AND NEW.type != 5) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET error = " +
+                   "    CASE" +
+                   "      WHEN NEW.type = 5 THEN error + 1" +
+                   "      ELSE error - 1" +
+                   "    END " +
+                   "  WHERE _id = NEW.thread_id; " +
+                   "END;");
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
+        Log.w(TAG, "Upgrading database from version " + oldVersion
+                + " to " + currentVersion + ".");
+
+        switch (oldVersion) {
+            case 24:
+                if (currentVersion <= 24) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion25(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 25:
+                if (currentVersion <= 25) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion26(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 26:
+                if (currentVersion <= 26) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion27(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 27:
+                if (currentVersion <= 27) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion28(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 28:
+                if (currentVersion <= 28) {
+                    return;
+                }
+
+                // Test whether this database file is from TC2 branch.
+                Cursor c = db.rawQuery("SELECT * FROM threads", null);
+                if (c != null) {
+                    try {
+                        c.getColumnIndexOrThrow("snippet_cs");
+                    } catch (IllegalArgumentException e) {
+                        // Column 'snippet_cs' doesn't exist, which means
+                        // this database file was maintained by TC2 branch
+                        // and its version is inconsistent.
+                        Log.w(TAG, "Upgrade database file from TC2!!!");
+                        db.beginTransaction();
+                        try {
+                            upgradeDatabaseToVersion28(db);
+                            db.setTransactionSuccessful();
+                        } catch (Throwable ex) {
+                            Log.e(TAG, ex.getMessage(), ex);
+                            break; // force to destroy all old data;
+                        } finally {
+                            db.endTransaction();
+                        }
+                    } finally {
+                        c.close();
+                    }
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion29(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 29:
+                if (currentVersion <= 29) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion30(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 30:
+                if (currentVersion <= 30) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion31(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 31:
+                if (currentVersion <= 31) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion32(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 32:
+                if (currentVersion <= 32) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion33(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 33:
+                if (currentVersion <= 33) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion34(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 34:
+                if (currentVersion <= 34) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion35(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 35:
+                if (currentVersion <= 35) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion36(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 36:
+                if (currentVersion <= 36) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion37(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 37:
+                if (currentVersion <= 37) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion38(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 38:
+                if (currentVersion <= 38) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion39(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 39:
+                if (currentVersion <= 39) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion40(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 40:
+                if (currentVersion <= 40) {
+                    return;
+                }
+
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion41(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break; // force to destroy all old data;
+                } finally {
+                    db.endTransaction();
+                }
+                // fall-through
+            case 41:
+                if (currentVersion <= 41) {
+                    return;
+                }
+                db.beginTransaction();
+                try {
+                    upgradeDatabaseToVersion42(db);
+                    db.setTransactionSuccessful();
+                } catch (Throwable ex) {
+                    Log.e(TAG, ex.getMessage(), ex);
+                    break;
+                } finally {
+                    db.endTransaction();
+                }
+                return;
+        }
+
+        Log.w(TAG, "Destroying all old data.");
+        dropCommonTriggers(db);
+        dropMmsTriggers(db);
+        dropCommonTables(db);
+        dropMmsTables(db);
+        dropSmsTables(db);
+        onCreate(db);
+    }
+
+    private void dropCommonTables(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS canonical_addresses");
+        db.execSQL("DROP TABLE IF EXISTS threads");
+        db.execSQL("DROP TABLE IF EXISTS " + MmsSmsProvider.TABLE_PENDING_MSG);
+    }
+
+    private void dropCommonTriggers(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_pdu");
+        db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_when_update_pdu");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_date_subject_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_date_subject_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_read_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_read_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS insert_mms_pending_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS insert_mms_pending_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS delete_mms_pending_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS delete_mms_pending_on_delete");
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_update_mms");
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete_mms");
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_move_mms");
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_update_sms");
+    }
+
+    private void dropSmsTables(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS sms");
+        db.execSQL("DROP TABLE IF EXISTS newSmsIndicator");
+        db.execSQL("DROP TABLE IF EXISTS raw");
+        db.execSQL("DROP TABLE IF EXISTS attachments");
+        db.execSQL("DROP TABLE IF EXISTS thread_ids");
+        db.execSQL("DROP TABLE IF EXISTS sr_pending");
+    }
+
+    private void dropMmsTables(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PDU + ";");
+        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_ADDR + ";");
+        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_PART + ";");
+        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_RATE + ";");
+        db.execSQL("DROP TABLE IF EXISTS " + MmsProvider.TABLE_DRM + ";");
+    }
+
+    private void dropMmsTriggers(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS part_cleanup;");
+        db.execSQL("DROP TRIGGER IF EXISTS addr_cleanup;");
+        db.execSQL("DROP TRIGGER IF EXISTS cleanup_delivery_and_read_report;");
+    }
+
+    private void upgradeDatabaseToVersion25(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE threads " +
+                   "ADD COLUMN type INTEGER NOT NULL DEFAULT 0;");
+    }
+
+    private void upgradeDatabaseToVersion26(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE threads " +
+                   "ADD COLUMN error INTEGER DEFAULT 0;");
+
+        // Do NOT use defined symbols when upgrading database
+        // because they may be changed and cannot be applied
+        // to old database.
+        db.execSQL("UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM pdu LEFT JOIN pending_msgs" +
+                   "     ON pdu.thread_id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 1 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM sms LEFT JOIN pending_msgs" +
+                   "     ON sms.thread_id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 0 AND err_type >= 10" +
+                   "     GROUP BY thread_id); ");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_update " +
+                   "  AFTER UPDATE OF err_type ON pending_msgs " +
+                   "BEGIN " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM pdu LEFT JOIN pending_msgs" +
+                   "     ON pdu.thread_id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 1 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM sms LEFT JOIN pending_msgs" +
+                   "     ON sms.thread_id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 0 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_delete " +
+                   "  AFTER DELETE ON pending_msgs " +
+                   "BEGIN " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM pdu LEFT JOIN pending_msgs" +
+                   "     ON pdu.thread_id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 1 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM sms LEFT JOIN pending_msgs" +
+                   "     ON sms.thread_id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 0 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion27(SQLiteDatabase db) {
+        db.execSQL("UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM pdu LEFT JOIN pending_msgs" +
+                   "     ON pdu._id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 1 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM sms LEFT JOIN pending_msgs" +
+                   "     ON sms._id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 0 AND err_type >= 10" +
+                   "     GROUP BY thread_id); ");
+
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_update " +
+                   "  AFTER UPDATE OF err_type ON pending_msgs " +
+                   "BEGIN " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM pdu LEFT JOIN pending_msgs" +
+                   "     ON pdu._id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 1 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM sms LEFT JOIN pending_msgs" +
+                   "     ON sms._id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 0 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_delete " +
+                   "  AFTER DELETE ON pending_msgs " +
+                   "BEGIN " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM pdu LEFT JOIN pending_msgs" +
+                   "     ON pdu._id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 1 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "UPDATE threads SET error = 1 WHERE _id IN" +
+                   "  (SELECT thread_id FROM sms LEFT JOIN pending_msgs" +
+                   "     ON sms._id = pending_msgs.msg_id" +
+                   "     WHERE proto_type = 0 AND err_type >= 10" +
+                   "     GROUP BY thread_id); " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion28(SQLiteDatabase db) {
+        db.execSQL("ALTER TABLE threads " +
+                   "ADD COLUMN snippet_cs INTEGER NOT NULL DEFAULT 0;");
+
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_date_subject_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_read_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON pdu " +
+                   "  WHEN new.msg_box!=5 AND new.msg_box!=3" +
+                   "    AND (new.m_type=132 OR new.m_type=130 OR new.m_type=128) " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.sub, " +
+                   "    snippet_cs = new.sub_cs" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
+                   "  UPDATE OF date, sub, msg_box ON pdu " +
+                   "  WHEN new.msg_box!=5 AND new.msg_box!=3" +
+                   "    AND (new.m_type=132 OR new.m_type=130 OR new.m_type=128) " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.sub, " +
+                   "    snippet_cs = new.sub_cs" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
+                   "  UPDATE OF read ON pdu " +
+                   "  WHEN new.msg_box!=5 AND new.msg_box!=3" +
+                   "    AND (new.m_type=132 OR new.m_type=130 OR new.m_type=128) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_delete " +
+                   "AFTER DELETE ON sms " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000), " +
+                   "     snippet = (SELECT body FROM SMS ORDER BY date DESC LIMIT 1)" +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion29(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_date_subject_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_read_on_update");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON pdu " +
+                   "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.sub, " +
+                   "    snippet_cs = new.sub_cs" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
+                   "  UPDATE OF date, sub, msg_box ON pdu " +
+                   "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.sub, " +
+                   "    snippet_cs = new.sub_cs" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_read_on_update AFTER" +
+                   "  UPDATE OF read ON pdu " +
+                   "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                   "BEGIN " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion30(SQLiteDatabase db) {
+        // Since SQLite doesn't support altering constraints
+        // of an existing table, I have to create a new table
+        // with updated constraints, copy old data into this
+        // table, drop old table and then rename the new table
+        // to 'threads'.
+        db.execSQL("CREATE TABLE temp_threads (" +
+                   "_id INTEGER PRIMARY KEY," +
+                   "date INTEGER DEFAULT 0," +
+                   "subject TEXT," +
+                   "recipient_ids TEXT," +
+                   "snippet TEXT," +
+                   "snippet_cs INTEGER DEFAULT 0," +
+                   "read INTEGER DEFAULT 1," +
+                   "type INTEGER DEFAULT 0," +
+                   "error INTEGER DEFAULT 0);");
+        db.execSQL("INSERT INTO temp_threads SELECT * FROM threads;");
+        db.execSQL("DROP TABLE IF EXISTS threads;");
+        db.execSQL("ALTER TABLE temp_threads RENAME TO threads;");
+    }
+
+    private void upgradeDatabaseToVersion31(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
+
+        // Update threads table whenever a message in sms is deleted
+        // (Usually an abandoned draft.)
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_delete " +
+                   "AFTER DELETE ON sms " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET" +
+                   "    snippet = (SELECT snippet FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET" +
+                   "    snippet_cs = (SELECT snippet_cs FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "END;");
+
+        // Update threads table whenever a message in pdu is deleted
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_delete " +
+                   "AFTER DELETE ON pdu " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000)" +
+                   "  WHERE threads._id = old.thread_id;" +
+                   "  UPDATE threads SET" +
+                   "    snippet = (SELECT snippet FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET" +
+                   "    snippet_cs = (SELECT snippet_cs FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion32(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE IF NOT EXISTS rate (sent_time INTEGER);");
+    }
+
+    private void upgradeDatabaseToVersion33(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_update_mms " +
+                   "  AFTER UPDATE OF err_type ON pending_msgs " +
+                   "  WHEN (OLD.err_type < 10 AND NEW.err_type >= 10)" +
+                   "    OR (OLD.err_type >= 10 AND NEW.err_type < 10) " +
+                   "BEGIN" +
+                   "  UPDATE threads SET error = " +
+                   "    CASE" +
+                   "      WHEN NEW.err_type >= 10 THEN error + 1" +
+                   "      ELSE error - 1" +
+                   "    END " +
+                   "  WHERE _id =" +
+                   "   (SELECT DISTINCT thread_id" +
+                   "    FROM pdu" +
+                   "    WHERE _id = NEW.msg_id); " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_delete_mms " +
+                   "  BEFORE DELETE ON pdu" +
+                   "  WHEN OLD._id IN (SELECT DISTINCT msg_id" +
+                   "                   FROM pending_msgs" +
+                   "                   WHERE err_type >= 10) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET error = error - 1" +
+                   "  WHERE _id = OLD.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_update_sms " +
+                   "  AFTER UPDATE OF type ON sms" +
+                   "  WHEN (OLD.type != 5 AND NEW.type = 5)" +
+                   "    OR (OLD.type = 5 AND NEW.type != 5) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET error = " +
+                   "    CASE" +
+                   "      WHEN NEW.type = 5 THEN error + 1" +
+                   "      ELSE error - 1" +
+                   "    END " +
+                   "  WHERE _id = NEW.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER update_threads_error_on_delete_sms " +
+                   "  AFTER DELETE ON sms" +
+                   "  WHEN (OLD.type = 5) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET error = error - 1" +
+                   "  WHERE _id = OLD.thread_id; " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion34(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_date_subject_on_update");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.body," +
+                   "    snippet_cs = 0" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM sms" +
+                   "          WHERE read = 0" +
+                   "            AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1" +
+                   "      ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
+                   "  UPDATE OF date, body, msg_box" +
+                   "  ON sms " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.body," +
+                   "    snippet_cs = 0" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM sms" +
+                   "          WHERE read = 0" +
+                   "            AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1" +
+                   "      ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion35(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE temp_threads (" +
+                   "_id INTEGER PRIMARY KEY," +
+                   "date INTEGER DEFAULT 0," +
+                   "message_count INTEGER DEFAULT 0," +
+                   "recipient_ids TEXT," +
+                   "snippet TEXT," +
+                   "snippet_cs INTEGER DEFAULT 0," +
+                   "read INTEGER DEFAULT 1," +
+                   "type INTEGER DEFAULT 0," +
+                   "error INTEGER DEFAULT 0);");
+        db.execSQL("INSERT INTO temp_threads " +
+                   "SELECT _id, date, 0 AS message_count, recipient_ids," +
+                   "       snippet, snippet_cs, read, type, error " +
+                   "FROM threads;");
+        db.execSQL("DROP TABLE IF EXISTS threads;");
+        db.execSQL("ALTER TABLE temp_threads RENAME TO threads;");
+
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_delete");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_date_subject_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_date_subject_on_update");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON pdu " +
+                   "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.sub, " +
+                   "    snippet_cs = new.sub_cs" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.body," +
+                   "    snippet_cs = 0" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM sms" +
+                   "          WHERE read = 0" +
+                   "            AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1" +
+                   "      ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_delete " +
+                   "AFTER DELETE ON sms " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET" +
+                   "    snippet = (SELECT snippet FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET" +
+                   "    snippet_cs = (SELECT snippet_cs FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_delete " +
+                   "AFTER DELETE ON pdu " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000)" +
+                   "  WHERE threads._id = old.thread_id;" +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET" +
+                   "    snippet = (SELECT snippet FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET" +
+                   "    snippet_cs = (SELECT snippet_cs FROM" +
+                   "      (SELECT date * 1000 AS date, sub AS snippet," +
+                   "         sub_cs AS snippet_cs FROM pdu" +
+                   "       UNION SELECT date, body AS snippet, NULL AS snippet_cs" +
+                   "         FROM sms) ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
+                   "  UPDATE OF date, body, type" +
+                   "  ON sms " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.body," +
+                   "    snippet_cs = 0" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM sms" +
+                   "          WHERE read = 0" +
+                   "            AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1" +
+                   "      ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
+                   "  UPDATE OF date, sub, msg_box ON pdu " +
+                   "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                   "BEGIN" +
+                   "  UPDATE threads SET" +
+                   "    date = (strftime('%s','now') * 1000), " +
+                   "    snippet = new.sub, " +
+                   "    snippet_cs = new.sub_cs" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = new.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "  UPDATE threads SET read = " +
+                   "    CASE (SELECT COUNT(*)" +
+                   "          FROM pdu" +
+                   "          WHERE read = 0 AND thread_id = threads._id)" +
+                   "      WHEN 0 THEN 1 ELSE 0" +
+                   "    END" +
+                   "  WHERE threads._id = new.thread_id; " +
+                   "END;");
+    }
+
+    private void upgradeDatabaseToVersion36(SQLiteDatabase db) {
+        db.execSQL("CREATE TABLE IF NOT EXISTS drm (_id INTEGER PRIMARY KEY, _data TEXT);");
+        db.execSQL("CREATE TRIGGER IF NOT EXISTS drm_file_cleanup DELETE ON drm " +
+                   "BEGIN SELECT _DELETE_FILE(old._data); END;");
+    }
+
+    private void upgradeDatabaseToVersion37(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_delete");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_delete " +
+                   "AFTER DELETE ON sms " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET snippet = " +
+                   "   (SELECT snippet FROM" +
+                   "     (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
+                   "      UNION SELECT date, body AS snippet, thread_id FROM sms)" +
+                   "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = OLD.thread_id; " +
+                   "  UPDATE threads SET snippet_cs = " +
+                   "   (SELECT snippet_cs FROM" +
+                   "     (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
+                   "      UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
+                   "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = OLD.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_delete " +
+                   "AFTER DELETE ON pdu " +
+                   "BEGIN " +
+                   "  UPDATE threads SET " +
+                   "     date = (strftime('%s','now') * 1000)" +
+                   "  WHERE threads._id = old.thread_id;" +
+                   "  UPDATE threads SET message_count = " +
+                   "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND sms.type != 3) + " +
+                   "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                   "      ON threads._id = thread_id" +
+                   "      WHERE thread_id = old.thread_id" +
+                   "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                   "        AND msg_box != 3) " +
+                   "  WHERE threads._id = old.thread_id; " +
+                   "  UPDATE threads SET snippet = " +
+                   "   (SELECT snippet FROM" +
+                   "     (SELECT date * 1000 AS date, sub AS snippet, thread_id FROM pdu" +
+                   "      UNION SELECT date, body AS snippet, thread_id FROM sms)" +
+                   "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = OLD.thread_id; " +
+                   "  UPDATE threads SET snippet_cs = " +
+                   "   (SELECT snippet_cs FROM" +
+                   "     (SELECT date * 1000 AS date, sub_cs AS snippet_cs, thread_id FROM pdu" +
+                   "      UNION SELECT date, 0 AS snippet_cs, thread_id FROM sms)" +
+                   "    WHERE thread_id = OLD.thread_id ORDER BY date DESC LIMIT 1) " +
+                   "  WHERE threads._id = OLD.thread_id; " +
+                   "END;");
+
+        db.execSQL("CREATE TABLE temp_part (" +
+                   "_id INTEGER PRIMARY KEY," +
+                   "mid INTEGER," +
+                   "seq INTEGER DEFAULT 0," +
+                   "ct TEXT," +
+                   "name TEXT," +
+                   "chset INTEGER," +
+                   "cd TEXT," +
+                   "fn TEXT," +
+                   "cid TEXT," +
+                   "cl TEXT," +
+                   "ctt_s INTEGER," +
+                   "ctt_t TEXT," +
+                   "_data TEXT);");
+        db.execSQL("INSERT INTO temp_part SELECT * FROM part;");
+        db.execSQL("UPDATE temp_part SET seq='0';");
+        db.execSQL("UPDATE temp_part SET seq='-1' WHERE ct='application/smil';");
+        db.execSQL("DROP TABLE IF EXISTS part;");
+        db.execSQL("ALTER TABLE temp_part RENAME TO part;");
+    }
+
+    private void upgradeDatabaseToVersion38(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS part_file_cleanup;");
+        db.execSQL("DROP TRIGGER IF EXISTS drm_file_cleanup;");
+    }
+
+    private void upgradeDatabaseToVersion39(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_date_subject_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_date_subject_on_update");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.body," +
+                "    snippet_cs = 0" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is NULL) + " +
+                "     (SELECT COUNT(DISTINCT pdu.m_id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is not NULL) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM sms" +
+                "          WHERE read = 0" +
+                "            AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1" +
+                "      ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
+                "  UPDATE OF date, body, type" +
+                "  ON sms " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.body," +
+                "    snippet_cs = 0" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is NULL) + " +
+                "     (SELECT COUNT(DISTINCT pdu.m_id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is not NULL) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM sms" +
+                "          WHERE read = 0" +
+                "            AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1" +
+                "      ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON pdu " +
+                "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.sub, " +
+                "    snippet_cs = new.sub_cs" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is NULL) + " +
+                "     (SELECT COUNT(DISTINCT pdu.m_id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is not NULL) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM pdu" +
+                "          WHERE read = 0 AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1 ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
+                "  UPDATE OF date, sub, msg_box ON pdu " +
+                "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.sub, " +
+                "    snippet_cs = new.sub_cs" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is NULL) + " +
+                "     (SELECT COUNT(DISTINCT pdu.m_id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3 " +
+                "        AND pdu.m_id is not NULL) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM pdu" +
+                "          WHERE read = 0 AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1 ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+    }
+
+    private void upgradeDatabaseToVersion40(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_date_subject_on_update");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_on_insert");
+        db.execSQL("DROP TRIGGER IF EXISTS pdu_update_thread_date_subject_on_update");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_on_insert AFTER INSERT ON sms " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.body," +
+                "    snippet_cs = 0" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM sms" +
+                "          WHERE read = 0" +
+                "            AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1" +
+                "      ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+
+        db.execSQL("CREATE TRIGGER sms_update_thread_date_subject_on_update AFTER" +
+                "  UPDATE OF date, body, type" +
+                "  ON sms " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.body," +
+                "    snippet_cs = 0" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM sms" +
+                "          WHERE read = 0" +
+                "            AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1" +
+                "      ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_on_insert AFTER INSERT ON pdu " +
+                "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.sub, " +
+                "    snippet_cs = new.sub_cs" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM pdu" +
+                "          WHERE read = 0 AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1 ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+
+        db.execSQL("CREATE TRIGGER pdu_update_thread_date_subject_on_update AFTER" +
+                "  UPDATE OF date, sub, msg_box ON pdu " +
+                "  WHEN new.m_type=132 OR new.m_type=130 OR new.m_type=128 " +
+                "BEGIN" +
+                "  UPDATE threads SET" +
+                "    date = (strftime('%s','now') * 1000), " +
+                "    snippet = new.sub, " +
+                "    snippet_cs = new.sub_cs" +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET message_count = " +
+                "     (SELECT COUNT(sms._id) FROM sms LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND sms.type != 3) + " +
+                "     (SELECT COUNT(pdu._id) FROM pdu LEFT JOIN threads " +
+                "      ON threads._id = thread_id" +
+                "      WHERE thread_id = new.thread_id" +
+                "        AND (m_type=132 OR m_type=130 OR m_type=128)" +
+                "        AND msg_box != 3) " +
+                "  WHERE threads._id = new.thread_id; " +
+                "  UPDATE threads SET read = " +
+                "    CASE (SELECT COUNT(*)" +
+                "          FROM pdu" +
+                "          WHERE read = 0 AND thread_id = threads._id)" +
+                "      WHEN 0 THEN 1 ELSE 0" +
+                "    END" +
+                "  WHERE threads._id = new.thread_id; " +
+                "END;");
+    }
+
+    private void upgradeDatabaseToVersion41(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_move_mms");
+        db.execSQL("CREATE TRIGGER update_threads_error_on_move_mms " +
+                   "  BEFORE UPDATE OF msg_box ON pdu " +
+                   "  WHEN (OLD.msg_box = 4 AND NEW.msg_box != 4) " +
+                   "  AND (OLD._id IN (SELECT DISTINCT msg_id" +
+                   "                   FROM pending_msgs" +
+                   "                   WHERE err_type >= 10)) " +
+                   "BEGIN " +
+                   "  UPDATE threads SET error = error - 1" +
+                   "  WHERE _id = OLD.thread_id; " +
+                   "END;");
+    }
+    
+    private void upgradeDatabaseToVersion42(SQLiteDatabase db) {
+        db.execSQL("DROP TRIGGER IF EXISTS sms_update_thread_on_delete");
+        db.execSQL("DROP TRIGGER IF EXISTS delete_obsolete_threads_sms");
+        db.execSQL("DROP TRIGGER IF EXISTS update_threads_error_on_delete_sms");
+    }
+}
diff --git a/src/com/android/providers/telephony/MmsSmsProvider.java b/src/com/android/providers/telephony/MmsSmsProvider.java
new file mode 100644
index 0000000..b27e332
--- /dev/null
+++ b/src/com/android/providers/telephony/MmsSmsProvider.java
@@ -0,0 +1,1019 @@
+/*
+ * Copyright (C) 2008 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.pdu.PduHeaders;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.Telephony.CanonicalAddressesColumns;
+import android.provider.Telephony.Mms;
+import android.provider.Telephony.MmsSms;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.Threads;
+import android.provider.Telephony.ThreadsColumns;
+import android.provider.Telephony.MmsSms.PendingMessages;
+import android.provider.Telephony.Sms.Conversations;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This class provides the ability to query the MMS and SMS databases
+ * at the same time, mixing messages from both in a single thread
+ * (A.K.A. conversation).
+ *
+ * A virtual column, MmsSms.TYPE_DISCRIMINATOR_COLUMN, may be
+ * requested in the projection for a query.  Its value is either "mms"
+ * or "sms", depending on whether the message represented by the row
+ * is an MMS message or an SMS message, respectively.
+ *
+ * This class also provides the ability to find out what addresses
+ * participated in a particular thread.  It doesn't support updates
+ * for either of these.
+ *
+ * This class provides a way to allocate and retrieve thread IDs.
+ * This is done atomically through a query.  There is no insert URI
+ * for this.
+ *
+ * Finally, this class provides a way to delete or update all messages
+ * in a thread.
+ */
+public class MmsSmsProvider extends ContentProvider {
+    private static final UriMatcher URI_MATCHER =
+            new UriMatcher(UriMatcher.NO_MATCH);
+    private static final String LOG_TAG = "MmsSmsProvider";
+
+    private static final String NO_DELETES_INSERTS_OR_UPDATES =
+            "MmsSmsProvider does not support deletes, inserts, or updates for this URI.";
+    private static final int URI_CONVERSATIONS              = 0;
+    private static final int URI_CONVERSATIONS_MESSAGES     = 1;
+    private static final int URI_CONVERSATIONS_RECIPIENTS   = 2;
+    private static final int URI_MESSAGES_BY_PHONE          = 3;
+    private static final int URI_THREAD_ID                  = 4;
+    private static final int URI_CANONICAL_ADDRESS          = 5;
+    private static final int URI_PENDING_MSG                = 6;
+    private static final int URI_COMPLETE_CONVERSATIONS     = 7;
+    private static final int URI_UNDELIVERED_MSG            = 8;
+    private static final int URI_CONVERSATIONS_SUBJECT      = 9;
+    private static final int URI_NOTIFICATIONS              = 10;
+    private static final int URI_OBSOLETE_THREADS           = 11;
+    private static final int URI_DRAFT                      = 12;
+
+    /**
+     * the name of the table that is used to store the queue of
+     * messages(both MMS and SMS) to be sent/downloaded.
+     */
+    public static final String TABLE_PENDING_MSG = "pending_msgs";
+
+    // These constants are used to construct union queries across the
+    // MMS and SMS base tables.
+
+    // These are the columns that appear in both the MMS ("pdu") and
+    // SMS ("sms") message tables.
+    private static final String[] MMS_SMS_COLUMNS =
+            { BaseColumns._ID, Mms.DATE, Mms.READ, Mms.THREAD_ID };
+
+    // These are the columns that appear only in the MMS message
+    // table.
+    private static final String[] MMS_ONLY_COLUMNS = {
+        Mms.CONTENT_CLASS, Mms.CONTENT_LOCATION, Mms.CONTENT_TYPE,
+        Mms.DELIVERY_REPORT, Mms.EXPIRY, Mms.MESSAGE_CLASS, Mms.MESSAGE_ID,
+        Mms.MESSAGE_SIZE, Mms.MESSAGE_TYPE, Mms.MESSAGE_BOX, Mms.PRIORITY,
+        Mms.READ_STATUS, Mms.RESPONSE_STATUS, Mms.RESPONSE_TEXT,
+        Mms.RETRIEVE_STATUS, Mms.RETRIEVE_TEXT_CHARSET, Mms.REPORT_ALLOWED,
+        Mms.READ_REPORT, Mms.STATUS, Mms.SUBJECT, Mms.SUBJECT_CHARSET,
+        Mms.TRANSACTION_ID, Mms.MMS_VERSION };
+
+    // These are the columns that appear only in the SMS message
+    // table.
+    private static final String[] SMS_ONLY_COLUMNS =
+            { "address", "body", "person", "reply_path_present",
+              "service_center", "status", "subject", "type" };
+
+    // These are all the columns that appear in the "threads" table.
+    private static final String[] THREADS_COLUMNS = {
+        BaseColumns._ID,
+        ThreadsColumns.DATE,
+        ThreadsColumns.RECIPIENT_IDS,
+        ThreadsColumns.MESSAGE_COUNT
+    };
+
+    // These are all the columns that appear in the MMS and SMS
+    // message tables.
+    private static final String[] UNION_COLUMNS =
+            new String[MMS_SMS_COLUMNS.length
+                       + MMS_ONLY_COLUMNS.length
+                       + SMS_ONLY_COLUMNS.length];
+
+    // These are all the columns that appear in the MMS table.
+    private static final Set<String> MMS_COLUMNS = new HashSet<String>();
+
+    // These are all the columns that appear in the SMS table.
+    private static final Set<String> SMS_COLUMNS = new HashSet<String>();
+
+    private static final String VND_ANDROID_DIR_MMS_SMS =
+            "vnd.android-dir/mms-sms";
+
+    private static final String[] ID_PROJECTION = { BaseColumns._ID };
+
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    private static final String SMS_CONVERSATION_CONSTRAINT = "(" +
+            Sms.TYPE + " != " + Sms.MESSAGE_TYPE_DRAFT + ")";
+
+    private static final String MMS_CONVERSATION_CONSTRAINT = "(" +
+            Mms.MESSAGE_BOX + " != " + Mms.MESSAGE_BOX_DRAFTS + " AND (" +
+            Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_SEND_REQ + " OR " +
+            Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_RETRIEVE_CONF + " OR " +
+            Mms.MESSAGE_TYPE + " = " + PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND + "))";
+
+    private static final String AUTHORITY = "mms-sms";
+
+    static {
+        URI_MATCHER.addURI(AUTHORITY, "conversations", URI_CONVERSATIONS);
+        URI_MATCHER.addURI(AUTHORITY, "complete-conversations", URI_COMPLETE_CONVERSATIONS);
+
+        // In these patterns, "#" is the thread ID.
+        URI_MATCHER.addURI(
+                AUTHORITY, "conversations/#", URI_CONVERSATIONS_MESSAGES);
+        URI_MATCHER.addURI(
+                AUTHORITY, "conversations/#/recipients",
+                URI_CONVERSATIONS_RECIPIENTS);
+
+        URI_MATCHER.addURI(
+                AUTHORITY, "conversations/#/subject",
+                URI_CONVERSATIONS_SUBJECT);
+
+        // URI for deleting obsolete threads.
+        URI_MATCHER.addURI(AUTHORITY, "conversations/obsolete", URI_OBSOLETE_THREADS);
+
+        URI_MATCHER.addURI(
+                AUTHORITY, "messages/byphone/*",
+                URI_MESSAGES_BY_PHONE);
+
+        // In this pattern, two query parameter names are expected:
+        // "subject" and "recipient."  Multiple "recipient" parameters
+        // may be present.
+        URI_MATCHER.addURI(AUTHORITY, "threadID", URI_THREAD_ID);
+
+        // Use this pattern to query the canonical address by given ID.
+        URI_MATCHER.addURI(AUTHORITY, "canonical-address/#", URI_CANONICAL_ADDRESS);
+
+        // In this pattern, two query parameters may be supplied:
+        // "protocol" and "message." For example:
+        //   content://mms-sms/pending?
+        //       -> Return all pending messages;
+        //   content://mms-sms/pending?protocol=sms
+        //       -> Only return pending SMs;
+        //   content://mms-sms/pending?protocol=mms&message=1
+        //       -> Return the the pending MM which ID equals '1'.
+        //
+        URI_MATCHER.addURI(AUTHORITY, "pending", URI_PENDING_MSG);
+
+        // Use this pattern to get a list of undelivered messages.
+        URI_MATCHER.addURI(AUTHORITY, "undelivered", URI_UNDELIVERED_MSG);
+
+        // Use this pattern to see what delivery status reports (for
+        // both MMS and SMS) have not been delivered to the user.
+        URI_MATCHER.addURI(AUTHORITY, "notifications", URI_NOTIFICATIONS);
+
+        URI_MATCHER.addURI(AUTHORITY, "draft", URI_DRAFT);
+        initializeColumnSets();
+    }
+
+    private SQLiteOpenHelper mOpenHelper;
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = MmsSmsDatabaseHelper.getInstance(getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor cursor = null;
+
+        switch(URI_MATCHER.match(uri)) {
+            case URI_COMPLETE_CONVERSATIONS:
+                cursor = getCompleteConversations(
+                        projection, selection, selectionArgs, sortOrder);
+                break;
+            case URI_CONVERSATIONS:
+                String simple = uri.getQueryParameter("simple");
+                if ((simple != null) && simple.equals("true")) {
+                    String threadType = uri.getQueryParameter("thread_type");
+                    if (!TextUtils.isEmpty(threadType)) {
+                        selection = concatSelections(
+                                selection, Threads.TYPE + "=" + threadType);
+                    }
+                    cursor = getSimpleConversations(
+                            projection, selection, selectionArgs, sortOrder);
+                } else {
+                    cursor = getConversations(
+                            projection, selection, selectionArgs, sortOrder);
+                }
+                break;
+            case URI_CONVERSATIONS_MESSAGES:
+                cursor = getConversationMessages(
+                        uri.getPathSegments().get(1), projection, selection,
+                        selectionArgs, sortOrder);
+                break;
+            case URI_CONVERSATIONS_RECIPIENTS:
+                cursor = getConversationById(
+                        uri.getPathSegments().get(1), projection, selection,
+                        selectionArgs, sortOrder);
+                break;
+            case URI_CONVERSATIONS_SUBJECT:
+                cursor = getConversationById(
+                        uri.getPathSegments().get(1), projection, selection,
+                        selectionArgs, sortOrder);
+                break;
+            case URI_MESSAGES_BY_PHONE:
+                cursor = getMessagesByPhoneNumber(
+                        uri.getPathSegments().get(2), projection, selection,
+                        selectionArgs, sortOrder);
+                break;
+            case URI_THREAD_ID:
+                List<String> recipients = uri.getQueryParameters("recipient");
+
+                cursor = getThreadId(recipients);
+                break;
+            case URI_CANONICAL_ADDRESS: {
+                String extraSelection = "_id=" + uri.getPathSegments().get(1);
+                String finalSelection = TextUtils.isEmpty(selection)
+                        ? extraSelection : extraSelection + " AND " + selection;
+                cursor = db.query("canonical_addresses",
+                        new String[] {"address"}, finalSelection, selectionArgs,
+                        null, null, sortOrder);
+                break;
+            }
+            case URI_PENDING_MSG: {
+                String protoName = uri.getQueryParameter("protocol");
+                String msgId = uri.getQueryParameter("message");
+                int proto = TextUtils.isEmpty(protoName) ? -1
+                        : (protoName.equals("sms") ? MmsSms.SMS_PROTO : MmsSms.MMS_PROTO);
+
+                String extraSelection = (proto != -1) ?
+                        (PendingMessages.PROTO_TYPE + "=" + proto) : " 0=0 ";
+                if (!TextUtils.isEmpty(msgId)) {
+                    extraSelection += " AND " + PendingMessages.MSG_ID + "=" + msgId;
+                }
+
+                String finalSelection = TextUtils.isEmpty(selection)
+                        ? extraSelection : ("(" + extraSelection + ") AND " + selection);
+                String finalOrder = TextUtils.isEmpty(sortOrder)
+                        ? PendingMessages.DUE_TIME : sortOrder;
+                cursor = db.query(TABLE_PENDING_MSG, null,
+                        finalSelection, selectionArgs, null, null, finalOrder);
+                break;
+            }
+            case URI_UNDELIVERED_MSG: {
+                cursor = getUndeliveredMessages(projection, selection,
+                        selectionArgs, sortOrder);
+                break;
+            }
+            case URI_DRAFT: {
+                cursor = getDraftThread(projection, selection, selectionArgs, sortOrder);
+                break;
+            }
+            default:
+                throw new IllegalStateException("Unrecognized URI:" + uri);
+        }
+
+        cursor.setNotificationUri(getContext().getContentResolver(), MmsSms.CONTENT_URI);
+        return cursor;
+    }
+
+    /**
+     * Return the canonical address ID for this address.
+     */
+    private long getSingleAddressId(String address) {
+        boolean isEmail = Mms.isEmailAddress(address);
+        String refinedAddress = isEmail ? address.toLowerCase() : address;
+        String selection =
+                isEmail
+                ? "address = ?"
+                : "PHONE_NUMBERS_EQUAL(address, ?)";
+        String[] selectionArgs = new String[] { refinedAddress };
+        Cursor cursor = null;
+
+        try {
+            SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+            cursor = db.query(
+                    "canonical_addresses", ID_PROJECTION,
+                    selection, selectionArgs, null, null, null);
+
+            if (cursor.getCount() == 0) {
+                ContentValues contentValues = new ContentValues(1);
+                contentValues.put(CanonicalAddressesColumns.ADDRESS, refinedAddress);
+
+                db = mOpenHelper.getWritableDatabase();
+                return db.insert("canonical_addresses",
+                        CanonicalAddressesColumns.ADDRESS, contentValues);
+            }
+
+            if (cursor.moveToFirst()) {
+                return cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID));
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return -1L;
+    }
+
+    /**
+     * Return the canonical address IDs for these addresses.
+     */
+    private Set<Long> getAddressIds(List<String> addresses) {
+        Set<Long> result = new HashSet<Long>(addresses.size());
+
+        for (String address : addresses) {
+            if (!address.equals(PduHeaders.FROM_INSERT_ADDRESS_TOKEN_STR)) {
+                long id = getSingleAddressId(address);
+                if (id != -1L) {
+                    result.add(id);
+                } else {
+                    Log.e(LOG_TAG, "Address ID not found for: " + address);
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Return a sorted array of the given Set of Longs.
+     */
+    private long[] getSortedSet(Set<Long> numbers) {
+        int size = numbers.size();
+        long[] result = new long[size];
+        int i = 0;
+
+        for (Long number : numbers) {
+            result[i++] = number;
+        }
+        Arrays.sort(result);
+        return result;
+    }
+
+    /**
+     * Return a String of the numbers in the given array, in order,
+     * separated by spaces.
+     */
+    private String getSpaceSeparatedNumbers(long[] numbers) {
+        int size = numbers.length;
+        StringBuilder buffer = new StringBuilder();
+
+        for (int i = 0; i < size; i++) {
+            if (i != 0) {
+                buffer.append(' ');
+            }
+            buffer.append(numbers[i]);
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Insert a record for a new thread.
+     */
+    private void insertThread(String recipientIds, int numberOfRecipients) {
+        ContentValues values = new ContentValues(4);
+
+        long date = System.currentTimeMillis();
+        values.put(ThreadsColumns.DATE, date - date % 1000);
+        values.put(ThreadsColumns.RECIPIENT_IDS, recipientIds);
+        if (numberOfRecipients > 1) {
+            values.put(Threads.TYPE, Threads.BROADCAST_THREAD);
+        }
+        values.put(ThreadsColumns.MESSAGE_COUNT, 0);
+
+        mOpenHelper.getWritableDatabase().insert("threads", null, values);
+        getContext().getContentResolver().notifyChange(MmsSms.CONTENT_URI, null);
+    }
+
+    /**
+     * Return the thread ID for this list of
+     * recipients IDs.  If no thread exists with this ID, create
+     * one and return it.  Callers should always use
+     * Threads.getThreadId to access this information.
+     */
+    private synchronized Cursor getThreadId(List<String> recipients) {
+        String recipientIds =
+                getSpaceSeparatedNumbers(
+                        getSortedSet(getAddressIds(recipients)));
+        String THREAD_QUERY = "SELECT _id FROM threads " +
+                "WHERE recipient_ids = ?";
+
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor cursor = db.rawQuery(THREAD_QUERY, new String[] { recipientIds });
+
+        if (cursor.getCount() == 0) {
+            cursor.close();
+            insertThread(recipientIds, recipients.size());
+            db = mOpenHelper.getReadableDatabase();  // In case insertThread closed it
+            cursor = db.rawQuery(THREAD_QUERY, new String[] { recipientIds });
+        }
+
+        return cursor;
+    }
+
+    private static String concatSelections(String selection1, String selection2) {
+        if (TextUtils.isEmpty(selection1)) {
+            return selection2;
+        } else if (TextUtils.isEmpty(selection2)) {
+            return selection1;
+        } else {
+            return selection1 + " AND " + selection2;
+        }
+    }
+
+    /**
+     * If a null projection is given, return the union of all columns
+     * in both the MMS and SMS messages tables.  Otherwise, return the
+     * given projection.
+     */
+    private static String[] handleNullMessageProjection(
+            String[] projection) {
+        return projection == null ? UNION_COLUMNS : projection;
+    }
+
+    /**
+     * If a null projection is given, return the set of all columns in
+     * the threads table.  Otherwise, return the given projection.
+     */
+    private static String[] handleNullThreadsProjection(
+            String[] projection) {
+        return projection == null ? THREADS_COLUMNS : projection;
+    }
+
+    /**
+     * If a null sort order is given, return "normalized_date ASC".
+     * Otherwise, return the given sort order.
+     */
+    private static String handleNullSortOrder (String sortOrder) {
+        return sortOrder == null ? "normalized_date ASC" : sortOrder;
+    }
+
+    /**
+     * Return existing threads in the database.
+     */
+    private Cursor getSimpleConversations(String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        return mOpenHelper.getReadableDatabase().query("threads", projection,
+                selection, selectionArgs, null, null, " date DESC");
+    }
+
+    /**
+     * Return the thread which has draft in both MMS and SMS.
+     *
+     * Use this query:
+     *
+     *   SELECT ...
+     *     FROM (SELECT _id, thread_id, ...
+     *             FROM pdu
+     *             WHERE msg_box = 3 AND ...
+     *           UNION
+     *           SELECT _id, thread_id, ...
+     *             FROM sms
+     *             WHERE type = 3 AND ...
+     *          )
+     *   ;
+     */
+    private Cursor getDraftThread(String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        String[] innerProjection = new String[] {BaseColumns._ID, Conversations.THREAD_ID};
+        SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
+        SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
+
+        mmsQueryBuilder.setTables(MmsProvider.TABLE_PDU);
+        smsQueryBuilder.setTables(SmsProvider.TABLE_SMS);
+
+        String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection,
+                MMS_COLUMNS, 1, "mms",
+                concatSelections(selection, Mms.MESSAGE_BOX + "=" + Mms.MESSAGE_BOX_DRAFTS),
+                selectionArgs, null, null);
+        String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerProjection,
+                SMS_COLUMNS, 1, "sms",
+                concatSelections(selection, Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT),
+                selectionArgs, null, null);
+        SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
+
+        unionQueryBuilder.setDistinct(true);
+
+        String unionQuery = unionQueryBuilder.buildUnionQuery(
+                new String[] { mmsSubQuery, smsSubQuery }, null, null);
+
+        SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
+
+        outerQueryBuilder.setTables("(" + unionQuery + ")");
+
+        String outerQuery = outerQueryBuilder.buildQuery(
+                projection, null, null, null, null, sortOrder, null);
+
+        return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY);
+    }
+
+    /**
+     * Return the most recent message in each conversation in both MMS
+     * and SMS.
+     *
+     * Use this query:
+     *
+     *   SELECT ...
+     *     FROM (SELECT thread_id AS tid, date * 1000 AS normalized_date, ...
+     *             FROM pdu
+     *             WHERE msg_box != 3 AND ...
+     *             GROUP BY thread_id
+     *             HAVING date = MAX(date)
+     *           UNION
+     *           SELECT thread_id AS tid, date AS normalized_date, ...
+     *             FROM sms
+     *             WHERE ...
+     *             GROUP BY thread_id
+     *             HAVING date = MAX(date))
+     *     GROUP BY tid
+     *     HAVING normalized_date = MAX(normalized_date);
+     *
+     * The msg_box != 3 comparisons ensure that we don't include draft
+     * messages.
+     */
+    private Cursor getConversations(String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
+        SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
+
+        mmsQueryBuilder.setTables(MmsProvider.TABLE_PDU);
+        smsQueryBuilder.setTables(SmsProvider.TABLE_SMS);
+
+        String[] columns = handleNullMessageProjection(projection);
+        String[] innerMmsProjection = makeProjectionWithDateAndThreadId(
+                UNION_COLUMNS, 1000);
+        String[] innerSmsProjection = makeProjectionWithDateAndThreadId(
+                UNION_COLUMNS, 1);
+        String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection,
+                MMS_COLUMNS, 1, "mms",
+                concatSelections(selection, MMS_CONVERSATION_CONSTRAINT), selectionArgs,
+                "thread_id", "date = MAX(date)");
+        String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection,
+                SMS_COLUMNS, 1, "sms",
+                concatSelections(selection, SMS_CONVERSATION_CONSTRAINT), selectionArgs,
+                "thread_id", "date = MAX(date)");
+        SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
+
+        unionQueryBuilder.setDistinct(true);
+
+        String unionQuery = unionQueryBuilder.buildUnionQuery(
+                new String[] { mmsSubQuery, smsSubQuery }, null, null);
+
+        SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
+
+        outerQueryBuilder.setTables("(" + unionQuery + ")");
+
+        String outerQuery = outerQueryBuilder.buildQuery(
+                columns, null, null, "tid",
+                "normalized_date = MAX(normalized_date)", sortOrder, null);
+
+        return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY);
+    }
+
+    /**
+     * Return every message in each conversation in both MMS
+     * and SMS.
+     */
+    private Cursor getCompleteConversations(String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        String unionQuery = buildConversationQuery(
+                projection, selection, selectionArgs, sortOrder);
+
+        return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY);
+    }
+
+    /**
+     * Add normalized date and thread_id to the list of columns for an
+     * inner projection.  This is necessary so that the outer query
+     * can have access to these columns even if the caller hasn't
+     * requested them in the result.
+     */
+    private String[] makeProjectionWithDateAndThreadId(
+            String[] projection, int dateMultiple) {
+        int projectionSize = projection.length;
+        String[] result = new String[projectionSize + 2];
+
+        result[0] = "thread_id AS tid";
+        result[1] = "date * " + dateMultiple + " AS normalized_date";
+        for (int i = 0; i < projectionSize; i++) {
+            result[i + 2] = projection[i];
+        }
+        return result;
+    }
+
+    /**
+     * Return the union of MMS and SMS messages for this thread ID.
+     */
+    private Cursor getConversationMessages(
+            String threadIdString, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        try {
+            Long.parseLong(threadIdString);
+        } catch (NumberFormatException exception) {
+            Log.e(LOG_TAG, "Thread ID must be a Long.");
+            return null;
+        }
+
+        String finalSelection = concatSelections(
+                selection, "thread_id = " + threadIdString);
+        String unionQuery = buildConversationQuery(
+                projection, finalSelection, selectionArgs, sortOrder);
+
+        return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY);
+    }
+
+    /**
+     * Return the union of MMS and SMS messages whose recipients
+     * included this phone number.
+     *
+     * Use this query:
+     *
+     * SELECT ...
+     *   FROM pdu, (SELECT _id AS address_id
+     *              FROM addr
+     *              WHERE PHONE_NUMBERS_EQUAL(addr.address, '<phoneNumber>'))
+     *             AS matching_addresses
+     *   WHERE pdu._id = matching_addresses.address_id
+     * UNION
+     * SELECT ...
+     *   FROM sms
+     *   WHERE PHONE_NUMBERS_EQUAL(sms.address, '<phoneNumber>');
+     */
+    private Cursor getMessagesByPhoneNumber(
+            String phoneNumber, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        String escapedPhoneNumber = DatabaseUtils.sqlEscapeString(phoneNumber);
+        String finalMmsSelection =
+                concatSelections(
+                        selection,
+                        "pdu._id = matching_addresses.address_id");
+        String finalSmsSelection =
+                concatSelections(
+                        selection,
+                        "PHONE_NUMBERS_EQUAL(address, " +
+                        escapedPhoneNumber + ")");
+        SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
+        SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
+
+        mmsQueryBuilder.setDistinct(true);
+        smsQueryBuilder.setDistinct(true);
+        mmsQueryBuilder.setTables(
+                MmsProvider.TABLE_PDU +
+                ", (SELECT _id AS address_id " +
+                "FROM addr WHERE PHONE_NUMBERS_EQUAL(addr.address, " +
+                escapedPhoneNumber + ")) " +
+                "AS matching_addresses");
+        smsQueryBuilder.setTables(SmsProvider.TABLE_SMS);
+
+        String[] columns = handleNullMessageProjection(projection);
+        String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, MMS_COLUMNS,
+                0, "mms", finalMmsSelection, selectionArgs, null, null);
+        String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, columns, SMS_COLUMNS,
+                0, "sms", finalSmsSelection, selectionArgs, null, null);
+        SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
+
+        unionQueryBuilder.setDistinct(true);
+
+        String unionQuery = unionQueryBuilder.buildUnionQuery(
+                new String[] { mmsSubQuery, smsSubQuery }, sortOrder, null);
+
+        return mOpenHelper.getReadableDatabase().rawQuery(unionQuery, EMPTY_STRING_ARRAY);
+    }
+
+    /**
+     * Return the conversation of certain thread ID.
+     */
+    private Cursor getConversationById(
+            String threadIdString, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        try {
+            Long.parseLong(threadIdString);
+        } catch (NumberFormatException exception) {
+            Log.e(LOG_TAG, "Thread ID must be a Long.");
+            return null;
+        }
+
+        String extraSelection = "_id=" + threadIdString;
+        String finalSelection = concatSelections(selection, extraSelection);
+        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
+        String[] columns = handleNullThreadsProjection(projection);
+
+        queryBuilder.setDistinct(true);
+        queryBuilder.setTables("threads");
+        return queryBuilder.query(
+                mOpenHelper.getReadableDatabase(), columns, finalSelection,
+                selectionArgs, sortOrder, null, null);
+    }
+
+    private static String joinPduAndPendingMsgTables() {
+        return MmsProvider.TABLE_PDU + " LEFT JOIN " + TABLE_PENDING_MSG
+                + " ON pdu._id = pending_msgs.msg_id";
+    }
+
+    private static String[] createMmsProjection(String[] old) {
+        String[] newProjection = new String[old.length];
+        for (int i = 0; i < old.length; i++) {
+            if (old[i].equals(BaseColumns._ID)) {
+                newProjection[i] = "pdu._id";
+            } else {
+                newProjection[i] = old[i];
+            }
+        }
+        return newProjection;
+    }
+
+    private Cursor getUndeliveredMessages(
+            String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        String[] mmsProjection = createMmsProjection(projection);
+
+        SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
+        SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
+
+        mmsQueryBuilder.setTables(joinPduAndPendingMsgTables());
+        smsQueryBuilder.setTables(SmsProvider.TABLE_SMS);
+
+        String finalMmsSelection = concatSelections(
+                selection, Mms.MESSAGE_BOX + " = " + Mms.MESSAGE_BOX_OUTBOX);
+        String finalSmsSelection = concatSelections(
+                selection, "(" + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_OUTBOX
+                + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_FAILED
+                + " OR " + Sms.TYPE + " = " + Sms.MESSAGE_TYPE_QUEUED + ")");
+
+        String[] smsColumns = handleNullMessageProjection(projection);
+        String[] mmsColumns = handleNullMessageProjection(mmsProjection);
+        String[] innerMmsProjection = makeProjectionWithDateAndThreadId(
+                mmsColumns, 1000);
+        String[] innerSmsProjection = makeProjectionWithDateAndThreadId(
+                smsColumns, 1);
+
+        Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS);
+        columnsPresentInTable.add("pdu._id");
+        columnsPresentInTable.add(PendingMessages.ERROR_TYPE);
+        String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection,
+                columnsPresentInTable, 1, "mms", finalMmsSelection, selectionArgs,
+                null, null);
+        String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection,
+                SMS_COLUMNS, 1, "sms", finalSmsSelection, selectionArgs,
+                null, null);
+        SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
+
+        unionQueryBuilder.setDistinct(true);
+
+        String unionQuery = unionQueryBuilder.buildUnionQuery(
+                new String[] { smsSubQuery, mmsSubQuery }, null, null);
+
+        SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
+
+        outerQueryBuilder.setTables("(" + unionQuery + ")");
+
+        String outerQuery = outerQueryBuilder.buildQuery(
+                smsColumns, null, null, null, null, sortOrder, null);
+
+        return mOpenHelper.getReadableDatabase().rawQuery(outerQuery, EMPTY_STRING_ARRAY);
+    }
+
+    /**
+     * Add normalized date to the list of columns for an inner
+     * projection.
+     */
+    private static String[] makeProjectionWithNormalizedDate(
+            String[] projection, int dateMultiple) {
+        int projectionSize = projection.length;
+        String[] result = new String[projectionSize + 1];
+
+        result[0] = "date * " + dateMultiple + " AS normalized_date";
+        System.arraycopy(projection, 0, result, 1, projectionSize);
+        return result;
+    }
+
+    private static String buildConversationQuery(String[] projection,
+            String selection, String[] selectionArgs, String sortOrder) {
+        String[] mmsProjection = createMmsProjection(projection);
+
+        SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
+        SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
+
+        mmsQueryBuilder.setDistinct(true);
+        smsQueryBuilder.setDistinct(true);
+        mmsQueryBuilder.setTables(joinPduAndPendingMsgTables());
+        smsQueryBuilder.setTables(SmsProvider.TABLE_SMS);
+
+        String[] smsColumns = handleNullMessageProjection(projection);
+        String[] mmsColumns = handleNullMessageProjection(mmsProjection);
+        String[] innerMmsProjection = makeProjectionWithNormalizedDate(mmsColumns, 1000);
+        String[] innerSmsProjection = makeProjectionWithNormalizedDate(smsColumns, 1);
+
+        Set<String> columnsPresentInTable = new HashSet<String>(MMS_COLUMNS);
+        columnsPresentInTable.add("pdu._id");
+        columnsPresentInTable.add(PendingMessages.ERROR_TYPE);
+
+        String mmsSelection = concatSelections(selection,
+                                Mms.MESSAGE_BOX + " != " + Mms.MESSAGE_BOX_DRAFTS);
+        String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerMmsProjection,
+                columnsPresentInTable, 0, "mms",
+                concatSelections(mmsSelection, MMS_CONVERSATION_CONSTRAINT),
+                selectionArgs, null, null);
+        String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(
+                MmsSms.TYPE_DISCRIMINATOR_COLUMN, innerSmsProjection, SMS_COLUMNS,
+                0, "sms", concatSelections(selection, SMS_CONVERSATION_CONSTRAINT),
+                selectionArgs, null, null);
+        SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder();
+
+        unionQueryBuilder.setDistinct(true);
+
+        String unionQuery = unionQueryBuilder.buildUnionQuery(
+                new String[] { smsSubQuery, mmsSubQuery },
+                handleNullSortOrder(sortOrder), null);
+
+        SQLiteQueryBuilder outerQueryBuilder = new SQLiteQueryBuilder();
+
+        outerQueryBuilder.setTables("(" + unionQuery + ")");
+
+        return outerQueryBuilder.buildQuery(
+                smsColumns, null, null, null, null, sortOrder, null);
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return VND_ANDROID_DIR_MMS_SMS;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection,
+            String[] selectionArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        Context context = getContext();
+        int affectedRows = 0;
+        
+        switch(URI_MATCHER.match(uri)) {
+            case URI_CONVERSATIONS_MESSAGES:
+                long threadId;
+                try {
+                    threadId = Long.parseLong(uri.getLastPathSegment());
+                } catch (NumberFormatException e) {
+                    Log.e(LOG_TAG, "Thread ID must be a long.");
+                    break;
+                }
+                affectedRows = deleteConversation(uri, selection, selectionArgs);
+                MmsSmsDatabaseHelper.updateThread(db, threadId);
+                break;
+            case URI_CONVERSATIONS:
+                affectedRows = MmsProvider.deleteMessages(context, db,
+                                        selection, selectionArgs, uri)
+                        + db.delete("sms", selection, selectionArgs);
+                MmsSmsDatabaseHelper.updateAllThreads(db, selection, selectionArgs);
+                break;
+            case URI_OBSOLETE_THREADS:
+                affectedRows = db.delete("threads",
+                        "_id NOT IN (SELECT DISTINCT thread_id FROM sms " +
+                        "UNION SELECT DISTINCT thread_id FROM pdu)", null);
+                break;
+            default:
+                throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES);
+        }
+
+        if (affectedRows > 0) {
+            context.getContentResolver().notifyChange(MmsSms.CONTENT_URI, null);
+        }
+        return affectedRows;
+    }
+
+    /**
+     * Delete the conversation with the given thread ID.
+     */
+    private int deleteConversation(Uri uri, String selection, String[] selectionArgs) {
+        String threadId = uri.getLastPathSegment();
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        String finalSelection = concatSelections(selection, "thread_id = " + threadId);
+        return MmsProvider.deleteMessages(getContext(), db, finalSelection,
+                                          selectionArgs, uri)
+                + db.delete("sms", finalSelection, selectionArgs);
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        throw new UnsupportedOperationException(NO_DELETES_INSERTS_OR_UPDATES);
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values,
+            String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int affectedRows = 0;
+        switch(URI_MATCHER.match(uri)) {
+            case URI_CONVERSATIONS_MESSAGES:
+                String threadIdString = uri.getPathSegments().get(1);
+                affectedRows = updateConversation(threadIdString, values,
+                        selection, selectionArgs);
+                break;
+            case URI_PENDING_MSG:
+                affectedRows = db.update(TABLE_PENDING_MSG, values, selection, null);
+                break;
+            default:
+                throw new UnsupportedOperationException(
+                        NO_DELETES_INSERTS_OR_UPDATES);
+        }
+
+        if (affectedRows > 0) {
+            getContext().getContentResolver().notifyChange(
+                    MmsSms.CONTENT_URI, null);
+        }
+        return affectedRows;
+    }
+
+    private int updateConversation(
+            String threadIdString, ContentValues values, String selection,
+            String[] selectionArgs) {
+        try {
+            Long.parseLong(threadIdString);
+        } catch (NumberFormatException exception) {
+            Log.e(LOG_TAG, "Thread ID must be a Long.");
+            return 0;
+        }
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        String finalSelection = concatSelections(selection, "thread_id=" + threadIdString);
+        return db.update(MmsProvider.TABLE_PDU, values, finalSelection, selectionArgs)
+                + db.update("sms", values, finalSelection, selectionArgs);
+    }
+
+    /**
+     * Construct Sets of Strings containing exactly the columns
+     * present in each table.  We will use this when constructing
+     * UNION queries across the MMS and SMS tables.
+     */
+    private static void initializeColumnSets() {
+        int commonColumnCount = MMS_SMS_COLUMNS.length;
+        int mmsOnlyColumnCount = MMS_ONLY_COLUMNS.length;
+        int smsOnlyColumnCount = SMS_ONLY_COLUMNS.length;
+        Set<String> unionColumns = new HashSet<String>();
+
+        for (int i = 0; i < commonColumnCount; i++) {
+            MMS_COLUMNS.add(MMS_SMS_COLUMNS[i]);
+            SMS_COLUMNS.add(MMS_SMS_COLUMNS[i]);
+            unionColumns.add(MMS_SMS_COLUMNS[i]);
+        }
+        for (int i = 0; i < mmsOnlyColumnCount; i++) {
+            MMS_COLUMNS.add(MMS_ONLY_COLUMNS[i]);
+            unionColumns.add(MMS_ONLY_COLUMNS[i]);
+        }
+        for (int i = 0; i < smsOnlyColumnCount; i++) {
+            SMS_COLUMNS.add(SMS_ONLY_COLUMNS[i]);
+            unionColumns.add(SMS_ONLY_COLUMNS[i]);
+        }
+
+        int i = 0;
+        for (String columnName : unionColumns) {
+            UNION_COLUMNS[i++] = columnName;
+        }
+    }
+}
diff --git a/src/com/android/providers/telephony/SmsProvider.java b/src/com/android/providers/telephony/SmsProvider.java
new file mode 100644
index 0000000..ee60142
--- /dev/null
+++ b/src/com/android/providers/telephony/SmsProvider.java
@@ -0,0 +1,712 @@
+/*
+ * Copyright (C) 2006 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.ContentResolver;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import com.android.internal.database.ArrayListCursor;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.Contacts;
+import android.provider.Telephony.Mms;
+import android.provider.Telephony.MmsSms;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.TextBasedSmsColumns;
+import android.provider.Telephony.Threads;
+import android.telephony.gsm.SmsManager;
+import android.telephony.gsm.SmsMessage;
+import android.text.TextUtils;
+import android.util.Config;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class SmsProvider extends ContentProvider {
+    private static final Uri NOTIFICATION_URI = Uri.parse("content://sms");
+    private static final Uri SIM_URI = Uri.parse("content://sms/sim");
+    static final String TABLE_SMS = "sms";
+    private static final String TABLE_RAW = "raw";
+    private static final String TABLE_SR_PENDING = "sr_pending";
+    
+    private static final Integer ONE = Integer.valueOf(1);
+
+    /**
+     * These are the columns that are available when reading SMS
+     * messages from the SIM.  Columns whose names begin with "is_"
+     * have either "true" or "false" as their values.
+     */
+    private final static String[] SIM_COLUMNS = new String[] {
+        // N.B.: These columns must appear in the same order as the
+        // calls to add appear in convertSimToSms.
+        "service_center_address",       // getServiceCenterAddress
+        "address",                      // getDisplayOriginatingAddress
+        "message_class",                // getMessageClass
+        "body",                         // getDisplayMessageBody
+        "date",                         // getTimestampMillis
+        "status",                       // getStatusOnSim
+        "index_on_sim",                 // getIndexOnSim
+        "is_status_report",             // isStatusReportMessage
+        "transport_type",               // Always "sms".
+        "type"                          // Always MESSAGE_TYPE_ALL.
+    };
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = MmsSmsDatabaseHelper.getInstance(getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri url, String[] projectionIn, String selection,
+            String[] selectionArgs, String sort) {
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+
+        // Generate the body of the query.
+        int match = sURLMatcher.match(url);
+        switch (match) {
+            case SMS_ALL:
+                constructQueryForBox(qb, Sms.MESSAGE_TYPE_ALL);
+                break;
+
+            case SMS_UNDELIVERED:
+                constructQueryForUndelivered(qb);
+                break;
+                
+            case SMS_FAILED:
+                constructQueryForBox(qb, Sms.MESSAGE_TYPE_FAILED);
+                break;
+
+            case SMS_QUEUED:
+                constructQueryForBox(qb, Sms.MESSAGE_TYPE_QUEUED);
+                break;
+                
+            case SMS_INBOX:
+                constructQueryForBox(qb, Sms.MESSAGE_TYPE_INBOX);
+                break;
+
+            case SMS_SENT:
+                constructQueryForBox(qb, Sms.MESSAGE_TYPE_SENT);
+                break;
+
+            case SMS_DRAFT:
+                constructQueryForBox(qb, Sms.MESSAGE_TYPE_DRAFT);
+                break;
+
+            case SMS_OUTBOX:
+                constructQueryForBox(qb, Sms.MESSAGE_TYPE_OUTBOX);
+                break;
+
+            case SMS_ALL_ID:
+                qb.setTables(TABLE_SMS);
+                qb.appendWhere("(_id = " + url.getPathSegments().get(0) + ")");
+                break;
+
+            case SMS_INBOX_ID:
+            case SMS_FAILED_ID:
+            case SMS_SENT_ID:
+            case SMS_DRAFT_ID:
+            case SMS_OUTBOX_ID:
+                qb.setTables(TABLE_SMS);
+                qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")");
+                break;
+
+            case SMS_CONVERSATIONS_ID:
+                int threadID;
+
+                try {
+                    threadID = Integer.parseInt(url.getPathSegments().get(1));
+                    if (Config.LOGD) {
+                        Log.d(TAG, "query conversations: threadID=" + threadID);
+                    }
+                }
+                catch (Exception ex) {
+                    Log.e(TAG,
+                          "Bad conversation thread id: "
+                          + url.getPathSegments().get(1));
+                    return null;
+                }
+
+                qb.setTables(TABLE_SMS);
+                qb.appendWhere("thread_id = " + threadID);
+                break;
+
+            case SMS_CONVERSATIONS:
+                qb.setTables("sms, (SELECT thread_id AS group_thread_id, MAX(date) AS group_date, COUNT(*) AS msg_count FROM sms GROUP BY thread_id) AS groups");
+                qb.appendWhere("sms.thread_id = groups.group_thread_id AND sms.date = groups.group_date");
+                qb.setProjectionMap(sConversationProjectionMap);
+                break;
+
+            case SMS_RAW_MESSAGE:
+                qb.setTables("raw");
+                break;
+
+            case SMS_STATUS_PENDING:
+                qb.setTables("sr_pending");
+                break;
+
+            case SMS_ATTACHMENT:
+                qb.setTables("attachments");
+                break;
+
+            case SMS_ATTACHMENT_ID:
+                qb.setTables("attachments");
+                qb.appendWhere(
+                        "(sms_id = " + url.getPathSegments().get(1) + ")");
+                break;
+
+            case SMS_QUERY_THREAD_ID:
+                qb.setTables("canonical_addresses");
+                if (projectionIn == null) {
+                    projectionIn = sIDProjection;
+                }
+                break;
+
+            case SMS_STATUS_ID:
+                qb.setTables(TABLE_SMS);
+                qb.appendWhere("(_id = " + url.getPathSegments().get(1) + ")");
+                break;
+
+            case SMS_ALL_SIM:
+                return getAllMessagesFromSim();
+
+            case SMS_SIM:
+                String messageIndexString = url.getPathSegments().get(1);
+
+                return getSingleMessageFromSim(messageIndexString);
+
+            default:
+                Log.e(TAG, "Invalid request: " + url);
+                return null;
+        }
+
+        String orderBy = null;
+
+        if (!TextUtils.isEmpty(sort)) {
+            orderBy = sort;
+        } else if (qb.getTables().equals(TABLE_SMS)) {
+            orderBy = Sms.DEFAULT_SORT_ORDER;
+        }
+
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor ret = qb.query(db, projectionIn, selection, selectionArgs,
+                              null, null, orderBy);
+
+        // TODO: Since the URLs are a mess, always use content://sms
+        ret.setNotificationUri(getContext().getContentResolver(),
+                NOTIFICATION_URI);
+        return ret;
+    }
+
+    private ArrayList<String> convertSimToSms(SmsMessage message) {
+        ArrayList result = new ArrayList();
+
+        // N.B.: These calls must appear in the same order as the
+        // columns appear in SIM_COLUMNS.
+        result.add(message.getServiceCenterAddress());
+        result.add(message.getDisplayOriginatingAddress());
+        result.add(message.getMessageClass().toString());
+        result.add(message.getDisplayMessageBody());
+        result.add(message.getTimestampMillis());
+        result.add(Sms.STATUS_NONE);
+        result.add(message.getIndexOnSim());
+        result.add(message.isStatusReportMessage());
+        result.add("sms");
+        result.add(TextBasedSmsColumns.MESSAGE_TYPE_ALL);
+        return result;
+    }
+
+    /**
+     * Return a Cursor containing just one message from the SIM.
+     */
+    private Cursor getSingleMessageFromSim(String messageIndexString) {
+        try {
+            int messageIndex = Integer.parseInt(messageIndexString);
+            SmsManager smsManager = SmsManager.getDefault();
+            ArrayList<SmsMessage> messages = smsManager.getAllMessagesFromSim();
+            ArrayList<ArrayList> singleRow = new ArrayList<ArrayList>();
+
+            SmsMessage message = messages.get(messageIndex);
+            if (message == null) {
+                throw new IllegalArgumentException(
+                        "Message not retrieved. ID: " + messageIndexString);
+            }
+            singleRow.add(convertSimToSms(message));
+            return withSimNotificationUri(
+                    new ArrayListCursor(SIM_COLUMNS, singleRow));
+        } catch (NumberFormatException exception) {
+            throw new IllegalArgumentException(
+                    "Bad SMS SIM ID: " + messageIndexString);
+        }
+    }
+
+    /**
+     * Return a Cursor listing all the messages stored on the SIM.
+     */
+    private Cursor getAllMessagesFromSim() {
+        SmsManager smsManager = SmsManager.getDefault();
+        ArrayList<SmsMessage> messages = smsManager.getAllMessagesFromSim();
+        ArrayList<ArrayList> rows = new ArrayList<ArrayList>();
+
+        for (int count = messages.size(), i = 0; i < count; i++) {
+            SmsMessage message = messages.get(i);
+            if (message != null) {
+                rows.add(convertSimToSms(message));
+            }
+        }
+        return withSimNotificationUri(new ArrayListCursor(SIM_COLUMNS, rows));
+    }
+
+    private Cursor withSimNotificationUri(Cursor cursor) {
+        cursor.setNotificationUri(getContext().getContentResolver(),
+                                  SIM_URI);
+        return cursor;
+    }
+
+    private void constructQueryForBox(SQLiteQueryBuilder qb, int type) {
+        qb.setTables(TABLE_SMS);
+
+        if (type != Sms.MESSAGE_TYPE_ALL) {
+            qb.appendWhere("type=" + type);
+        }
+    }
+
+    private void constructQueryForUndelivered(SQLiteQueryBuilder qb) {
+        qb.setTables(TABLE_SMS);
+
+        qb.appendWhere("(type=" + Sms.MESSAGE_TYPE_OUTBOX +
+                       " OR type=" + Sms.MESSAGE_TYPE_FAILED +
+                       " OR type=" + Sms.MESSAGE_TYPE_QUEUED + ")");
+    }
+    
+    @Override
+    public String getType(Uri url) {
+        switch (url.getPathSegments().size()) {
+        case 0:
+            return VND_ANDROID_DIR_SMS;
+            case 1:
+                try {
+                    Integer.parseInt(url.getPathSegments().get(0));
+                    return VND_ANDROID_SMS;
+                } catch (NumberFormatException ex) {
+                    return VND_ANDROID_DIR_SMS;
+                }
+            case 2:
+                // TODO: What about "threadID"?
+                if (url.getPathSegments().get(0).equals("conversations")) {
+                    return VND_ANDROID_SMSCHAT;
+                } else {
+                    return VND_ANDROID_SMS;
+                }
+        }
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri url, ContentValues initialValues) {
+        ContentValues values;
+        long rowID;
+        int type = Sms.MESSAGE_TYPE_ALL;
+
+        int match = sURLMatcher.match(url);
+        if (Config.LOGD) {
+            Log.d(TAG, "insert url=" + url + ", match=" + match);
+        }
+
+        String table = TABLE_SMS;
+
+        switch (match) {
+            case SMS_ALL:
+                Integer typeObj = initialValues.getAsInteger(Sms.TYPE);
+                if (typeObj != null) {
+                    type = typeObj.intValue();
+                } else {
+                    // default to inbox
+                    type = Sms.MESSAGE_TYPE_INBOX;
+                }
+                break;
+
+            case SMS_INBOX:
+                type = Sms.MESSAGE_TYPE_INBOX;
+                break;
+
+            case SMS_FAILED:
+                type = Sms.MESSAGE_TYPE_FAILED;
+                break;
+
+            case SMS_QUEUED:
+                type = Sms.MESSAGE_TYPE_QUEUED;
+                break;
+                
+            case SMS_SENT:
+                type = Sms.MESSAGE_TYPE_SENT;
+                break;
+
+            case SMS_DRAFT:
+                type = Sms.MESSAGE_TYPE_DRAFT;
+                break;
+
+            case SMS_OUTBOX:
+                type = Sms.MESSAGE_TYPE_OUTBOX;
+                break;
+
+            case SMS_RAW_MESSAGE:
+                table = "raw";
+                break;
+
+            case SMS_STATUS_PENDING:
+                table = "sr_pending";
+                break;
+
+            case SMS_ATTACHMENT:
+                table = "attachments";
+                break;
+
+            case SMS_NEW_THREAD_ID:
+                table = "canonical_addresses";
+                break;
+
+            default:
+                Log.e(TAG, "Invalid request: " + url);
+                return null;
+        }
+
+        if (table.equals(TABLE_SMS)) {
+            boolean addDate = false;
+            boolean addType = false;
+
+            // Make sure that the date and type are set
+            if (initialValues == null) {
+                values = new ContentValues(1);
+                addDate = true;
+                addType = true;
+            } else {
+                values = new ContentValues(initialValues);
+
+                if (!initialValues.containsKey(Sms.DATE)) {
+                    addDate = true;
+                }
+
+                if (!initialValues.containsKey(Sms.TYPE)) {
+                    addType = true;
+                }
+            }
+
+            if (addDate) {
+                values.put(Sms.DATE, new Long(System.currentTimeMillis()));
+            }
+
+            if (addType && (type != Sms.MESSAGE_TYPE_ALL)) {
+                values.put(Sms.TYPE, Integer.valueOf(type));
+            }
+
+            // thread_id
+            Long threadId = values.getAsLong(Sms.THREAD_ID);
+            String address = values.getAsString(Sms.ADDRESS);
+
+            if (((threadId == null) || (threadId == 0)) && (address != null)) {
+                values.put(Sms.THREAD_ID, Threads.getOrCreateThreadId(
+                                   getContext(), address));
+            }
+
+            if (type == Sms.MESSAGE_TYPE_INBOX) {
+                // Look up the person if not already filled in.
+                if ((values.getAsLong(Sms.PERSON) == null)
+                        && (!TextUtils.isEmpty(address))) {
+                    Cursor cursor = getContext().getContentResolver().query(
+                            Uri.withAppendedPath(
+                                    Contacts.Phones.CONTENT_FILTER_URL, address),
+                            new String[] { Contacts.Phones.PERSON_ID },
+                            null, null, null);
+                    if (cursor != null) {
+                        if (cursor.getCount() > 0) {
+                            cursor.moveToFirst();
+                            Long id = Long.valueOf(cursor.getLong(0));
+                                values.put(Sms.PERSON, id);
+                        }
+                        cursor.deactivate();
+                    }
+                }
+            } else {
+                // Mark all non-inbox messages read.
+                values.put(Sms.READ, ONE);
+            }
+        } else {
+            if (initialValues == null) {
+                values = new ContentValues(1);
+            } else {
+                values = initialValues;
+            }
+        }
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        rowID = db.insert(table, "body", values);
+        if (rowID > 0) {
+            Uri uri = Uri.parse("content://" + table + "/" + rowID);
+            notifyChange(uri);
+            return uri;
+        } else {
+            Log.e(TAG,
+                  "SmsProvider.insert: failed! " + values.toString());
+        }
+
+        return null;
+    }
+
+    @Override
+    public int delete(Uri url, String where, String[] whereArgs) {
+        int count;
+        int match = sURLMatcher.match(url);
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        switch (match) {
+            case SMS_ALL:
+                count = db.delete(TABLE_SMS, where, whereArgs);
+                if (count != 0) {
+                    // Don't update threads unless something changed.
+                    MmsSmsDatabaseHelper.updateAllThreads(db, where, whereArgs);
+                }
+                break;
+             
+            case SMS_ALL_ID:
+                try {
+                    int message_id = Integer.parseInt(url.getPathSegments().get(0));
+                    count = MmsSmsDatabaseHelper.deleteOneSms(db, message_id);
+                } catch (Exception e) {
+                    throw new IllegalArgumentException(
+                        "Bad message id: " + url.getPathSegments().get(0));
+                }
+                break;
+
+            case SMS_CONVERSATIONS_ID:
+                int threadID;
+
+                try {
+                    threadID = Integer.parseInt(url.getPathSegments().get(1));
+                } catch (Exception ex) {
+                    throw new IllegalArgumentException(
+                            "Bad conversation thread id: "
+                            + url.getPathSegments().get(1));
+                }
+
+                // delete the messages from the sms table
+                where = DatabaseUtils.concatenateWhere("thread_id=" + threadID, where);
+                count = db.delete(TABLE_SMS, where, whereArgs);
+                MmsSmsDatabaseHelper.updateThread(db, threadID);
+                break;
+
+            case SMS_RAW_MESSAGE:
+                count = db.delete("raw", where, whereArgs);
+                break;
+
+            case SMS_STATUS_PENDING:
+                count = db.delete("sr_pending", where, whereArgs);
+                break;
+
+            case SMS_SIM:
+                String messageIndexString = url.getPathSegments().get(1);
+
+                return deleteMessageFromSim(messageIndexString);
+
+            default:
+                throw new IllegalArgumentException("Unknown URL");
+        }
+
+        if (count > 0) {
+            notifyChange(url);
+        }
+        return count;
+    }
+
+    /**
+     * Delete the message at index from SIM.  Return true iff
+     * successful.
+     */
+    private int deleteMessageFromSim(String messageIndexString) {
+        SmsManager smsManager = SmsManager.getDefault();
+
+        try {
+            return smsManager.deleteMessageFromSim(
+                    Integer.parseInt(messageIndexString))
+                    ? 1 : 0;
+        } catch (NumberFormatException exception) {
+            throw new IllegalArgumentException(
+                    "Bad SMS SIM ID: " + messageIndexString);
+        } finally {
+            ContentResolver cr = getContext().getContentResolver();
+
+            cr.notifyChange(SIM_URI, null);
+        }
+    }
+
+    @Override
+    public int update(
+            Uri url, ContentValues values, String where, String[] whereArgs) {
+        int count = 0;
+        String table = TABLE_SMS;
+        String extraWhere = null;
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+        switch (sURLMatcher.match(url)) {
+            case SMS_RAW_MESSAGE:
+                table = TABLE_RAW;
+                break;
+
+            case SMS_STATUS_PENDING:
+                table = TABLE_SR_PENDING;
+                break;
+
+            case SMS_ALL:
+            case SMS_FAILED:
+            case SMS_QUEUED:
+            case SMS_INBOX:
+            case SMS_SENT:
+            case SMS_DRAFT:
+            case SMS_OUTBOX:
+            case SMS_CONVERSATIONS:
+                break;
+
+            case SMS_ALL_ID:
+                extraWhere = "_id=" + url.getPathSegments().get(0);
+                break;
+
+            case SMS_INBOX_ID:
+            case SMS_FAILED_ID:
+            case SMS_SENT_ID:
+            case SMS_DRAFT_ID:
+            case SMS_OUTBOX_ID:
+                extraWhere = "_id=" + url.getPathSegments().get(1);
+                break;
+
+            case SMS_CONVERSATIONS_ID: {
+                String threadId = url.getPathSegments().get(1);
+
+                try {
+                    Integer.parseInt(threadId);
+                } catch (Exception ex) {
+                    Log.e(TAG, "Bad conversation thread id: " + threadId);
+                    break;
+                }
+
+                extraWhere = "thread_id=" + threadId;
+                break;
+            }
+
+            case SMS_STATUS_ID:
+                extraWhere = "_id=" + url.getPathSegments().get(1);
+                break;  
+                    
+            default:
+                throw new UnsupportedOperationException(
+                        "URI " + url + " not supported");
+        }
+
+        where = DatabaseUtils.concatenateWhere(where, extraWhere);
+        count = db.update(table, values, where, whereArgs);
+
+        if (count > 0) {
+            notifyChange(url);
+        }
+        return count;
+    }
+
+    private void notifyChange(Uri uri) {
+        ContentResolver cr = getContext().getContentResolver();
+        cr.notifyChange(uri, null);
+        cr.notifyChange(MmsSms.CONTENT_URI, null);
+        cr.notifyChange(Uri.parse("content://mms-sms/conversations/"), null);
+    }
+
+    private SQLiteOpenHelper mOpenHelper;
+
+    private final static String TAG = "SmsProvider";
+    private final static String VND_ANDROID_SMS = "vnd.android.cursor.item/sms";
+    private final static String VND_ANDROID_SMSCHAT =
+            "vnd.android.cursor.item/sms-chat";
+    private final static String VND_ANDROID_DIR_SMS =
+            "vnd.android.cursor.dir/sms";
+
+    private static final HashMap<String, String> sConversationProjectionMap =
+            new HashMap<String, String>();
+    private static final String[] sIDProjection = new String[] { "_id" };
+
+    private static final int SMS_ALL = 0;
+    private static final int SMS_ALL_ID = 1;
+    private static final int SMS_INBOX = 2;
+    private static final int SMS_INBOX_ID = 3;
+    private static final int SMS_SENT = 4;
+    private static final int SMS_SENT_ID = 5;
+    private static final int SMS_DRAFT = 6;
+    private static final int SMS_DRAFT_ID = 7;
+    private static final int SMS_OUTBOX = 8;
+    private static final int SMS_OUTBOX_ID = 9;
+    private static final int SMS_CONVERSATIONS = 10;
+    private static final int SMS_CONVERSATIONS_ID = 11;
+    private static final int SMS_RAW_MESSAGE = 15;
+    private static final int SMS_ATTACHMENT = 16;
+    private static final int SMS_ATTACHMENT_ID = 17;
+    private static final int SMS_NEW_THREAD_ID = 18;
+    private static final int SMS_QUERY_THREAD_ID = 19;
+    private static final int SMS_STATUS_ID = 20;
+    private static final int SMS_STATUS_PENDING = 21;
+    private static final int SMS_ALL_SIM = 22;
+    private static final int SMS_SIM = 23;
+    private static final int SMS_FAILED = 24;
+    private static final int SMS_FAILED_ID = 25;
+    private static final int SMS_QUEUED = 26;
+    private static final int SMS_UNDELIVERED = 27;
+    
+    private static final UriMatcher sURLMatcher =
+            new UriMatcher(UriMatcher.NO_MATCH);
+
+    static {
+        sURLMatcher.addURI("sms", null, SMS_ALL);
+        sURLMatcher.addURI("sms", "#", SMS_ALL_ID);
+        sURLMatcher.addURI("sms", "inbox", SMS_INBOX);
+        sURLMatcher.addURI("sms", "inbox/#", SMS_INBOX_ID);
+        sURLMatcher.addURI("sms", "sent", SMS_SENT);
+        sURLMatcher.addURI("sms", "sent/#", SMS_SENT_ID);
+        sURLMatcher.addURI("sms", "draft", SMS_DRAFT);
+        sURLMatcher.addURI("sms", "draft/#", SMS_DRAFT_ID);
+        sURLMatcher.addURI("sms", "outbox", SMS_OUTBOX);
+        sURLMatcher.addURI("sms", "outbox/#", SMS_OUTBOX_ID);
+        sURLMatcher.addURI("sms", "undelivered", SMS_UNDELIVERED);        
+        sURLMatcher.addURI("sms", "failed", SMS_FAILED);
+        sURLMatcher.addURI("sms", "failed/#", SMS_FAILED_ID);
+        sURLMatcher.addURI("sms", "queued", SMS_QUEUED);
+        sURLMatcher.addURI("sms", "conversations", SMS_CONVERSATIONS);
+        sURLMatcher.addURI("sms", "conversations/*", SMS_CONVERSATIONS_ID);
+        sURLMatcher.addURI("sms", "raw", SMS_RAW_MESSAGE);
+        sURLMatcher.addURI("sms", "attachments", SMS_ATTACHMENT);
+        sURLMatcher.addURI("sms", "attachments/#", SMS_ATTACHMENT_ID);
+        sURLMatcher.addURI("sms", "threadID", SMS_NEW_THREAD_ID);
+        sURLMatcher.addURI("sms", "threadID/*", SMS_QUERY_THREAD_ID);
+        sURLMatcher.addURI("sms", "status/#", SMS_STATUS_ID);
+        sURLMatcher.addURI("sms", "sr_pending", SMS_STATUS_PENDING);
+        sURLMatcher.addURI("sms", "sim", SMS_ALL_SIM);
+        sURLMatcher.addURI("sms", "sim/#", SMS_SIM);
+
+        sConversationProjectionMap.put(Sms.Conversations.SNIPPET,
+                "body AS snippet");
+        sConversationProjectionMap.put("delta", null);
+    }
+}
diff --git a/src/com/android/providers/telephony/TelephonyProvider.java b/src/com/android/providers/telephony/TelephonyProvider.java
new file mode 100644
index 0000000..324d007
--- /dev/null
+++ b/src/com/android/providers/telephony/TelephonyProvider.java
@@ -0,0 +1,516 @@
+/* //device/content/providers/telephony/TelephonyProvider.java
+**
+** Copyright 2006, 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.*;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.Telephony;
+import android.util.Config;
+import android.util.Log;
+import android.util.Xml;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class TelephonyProvider extends ContentProvider
+{
+    private static final String DATABASE_NAME = "telephony.db";
+    // DATABASE_VERSION needs to be in-sync with version in apns.xml.
+    private static final int DATABASE_VERSION = 4 << 16;
+    private static final int URL_TELEPHONY = 1;
+    private static final int URL_CURRENT = 2;
+    private static final int URL_ID = 3;
+    private static final int URL_RESTOREAPN = 4;
+
+    private static final String TAG = "TelephonyProvider";
+    private static final String CARRIERS_TABLE = "carriers";
+    private static final String PARTNER_APNS_PATH = "etc/apns-conf.xml";
+
+    private static final UriMatcher s_urlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    private static final ContentValues s_currentNullMap;
+    private static final ContentValues s_currentSetMap;
+
+    static {
+        s_urlMatcher.addURI("telephony", "carriers", URL_TELEPHONY);
+        s_urlMatcher.addURI("telephony", "carriers/current", URL_CURRENT);
+        s_urlMatcher.addURI("telephony", "carriers/#", URL_ID);
+        s_urlMatcher.addURI("telephony", "carriers/restore", URL_RESTOREAPN);
+
+        s_currentNullMap = new ContentValues(1);
+        s_currentNullMap.put("current", (Long) null);
+
+        s_currentSetMap = new ContentValues(1);
+        s_currentSetMap.put("current", "1");
+    }
+
+    private static class DatabaseHelper extends SQLiteOpenHelper {
+        // Context to access resources with
+        private Context mContext;
+
+        /**
+         * DatabaseHelper helper class for loading apns into a database.
+         *
+         * @param parser the system-default parser for apns.xml
+         * @param confidential an optional parser for confidential APNS (stored separately)
+         */
+        public DatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, getVersion(context));
+            mContext = context;
+        }
+
+        private static int getVersion(Context context) {
+            // Get the database version, combining a static schema version and the XML version
+            Resources r = context.getResources();
+            XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns);
+            try {
+                XmlUtils.beginDocument(parser, "apns");
+                int publicversion = Integer.parseInt(parser.getAttributeValue(null, "version"));
+                return DATABASE_VERSION | publicversion;
+            } catch (Exception e) {
+                Log.e(TAG, "Can't get version of APN database", e);
+                return DATABASE_VERSION;
+            } finally {
+                parser.close();
+            }
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            // Set up the database schema
+            db.execSQL("CREATE TABLE " + CARRIERS_TABLE +
+                "(_id INTEGER PRIMARY KEY," +
+                    "name TEXT," +
+                    "numeric TEXT," +
+                    "mcc TEXT," +
+                    "mnc TEXT," +
+                    "apn TEXT," +
+                    "user TEXT," +
+                    "server TEXT," +
+                    "password TEXT," +
+                    "proxy TEXT," +
+                    "port TEXT," +
+                    "mmsproxy TEXT," +
+                    "mmsport TEXT," +
+                    "mmsc TEXT," +
+                    "type TEXT," +
+                    "current INTEGER);");
+
+            initDatabase(db);
+        }
+
+        private void initDatabase(SQLiteDatabase db) {
+            // Read internal APNS data
+            Resources r = mContext.getResources();
+            XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns);
+            int publicversion = -1;
+            try {
+                XmlUtils.beginDocument(parser, "apns");
+                publicversion = Integer.parseInt(parser.getAttributeValue(null, "version"));
+                loadApns(db, parser);
+            } catch (Exception e) {
+                Log.e(TAG, "Got execption while loading APN database.", e);
+            } finally {
+                parser.close();
+            }
+
+            // Read external APNS data (partner-provided)
+            XmlPullParser confparser = null;
+            // Environment.getRootDirectory() is a fancy way of saying ANDROID_ROOT or "/system".
+            File confFile = new File(Environment.getRootDirectory(), PARTNER_APNS_PATH);
+            FileReader confreader = null;
+            try {
+                confreader = new FileReader(confFile);
+                confparser = Xml.newPullParser();
+                confparser.setInput(confreader);
+                XmlUtils.beginDocument(confparser, "apns");
+
+                // Sanity check. Force internal version and confidential versions to agree
+                int confversion = Integer.parseInt(confparser.getAttributeValue(null, "version"));
+                if (publicversion != confversion) {
+                    throw new IllegalStateException("Internal APNS file version doesn't match "
+                            + confFile.getAbsolutePath());
+                }
+
+                loadApns(db, confparser);
+            } catch (FileNotFoundException e) {
+                // It's ok if the file isn't found. It means there isn't a confidential file
+                // Log.e(TAG, "File not found: '" + confFile.getAbsolutePath() + "'");
+            } catch (Exception e) {
+                Log.e(TAG, "Exception while parsing '" + confFile.getAbsolutePath() + "'", e);
+            } finally {
+                try { if (confreader != null) confreader.close(); } catch (IOException e) { }
+            }
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            db.execSQL("DROP TABLE IF EXISTS " + CARRIERS_TABLE + ";");
+            onCreate(db);
+        }
+
+        /**
+         * Gets the next row of apn values.
+         *
+         * @param parser the parser
+         * @return the row or null if it's not an apn
+         */
+        private ContentValues getRow(XmlPullParser parser) {
+            if (!"apn".equals(parser.getName())) {
+                return null;
+            }
+
+            ContentValues map = new ContentValues();
+
+            String mcc = parser.getAttributeValue(null, "mcc");
+            String mnc = parser.getAttributeValue(null, "mnc");
+            String numeric = mcc + mnc;
+
+            map.put(Telephony.Carriers.NUMERIC,numeric);
+            map.put(Telephony.Carriers.MCC, mcc);
+            map.put(Telephony.Carriers.MNC, mnc);
+            map.put(Telephony.Carriers.NAME, parser.getAttributeValue(null, "carrier"));
+            map.put(Telephony.Carriers.APN, parser.getAttributeValue(null, "apn"));
+            map.put(Telephony.Carriers.USER, parser.getAttributeValue(null, "user"));
+            map.put(Telephony.Carriers.SERVER, parser.getAttributeValue(null, "server"));
+            map.put(Telephony.Carriers.PASSWORD, parser.getAttributeValue(null, "password"));
+
+            // do not add NULL to the map so that insert() will set the default value
+            String proxy = parser.getAttributeValue(null, "proxy");
+            if (proxy != null) {
+                map.put(Telephony.Carriers.PROXY, proxy);
+            }
+            String port = parser.getAttributeValue(null, "port");
+            if (port != null) {
+                map.put(Telephony.Carriers.PORT, port);
+            }
+            String mmsproxy = parser.getAttributeValue(null, "mmsproxy");
+            if (mmsproxy != null) {
+                map.put(Telephony.Carriers.MMSPROXY, mmsproxy);
+            }
+            String mmsport = parser.getAttributeValue(null, "mmsport");
+            if (mmsport != null) {
+                map.put(Telephony.Carriers.MMSPORT, mmsport);
+            }
+            map.put(Telephony.Carriers.MMSC, parser.getAttributeValue(null, "mmsc"));
+            String type = parser.getAttributeValue(null, "type");
+            if (type != null) {
+                map.put(Telephony.Carriers.TYPE, type);
+            }
+
+            return map;
+        }
+
+        /*
+         * Loads apns from xml file into the database
+         *
+         * @param db the sqlite database to write to
+         * @param parser the xml parser
+         *
+         */
+        private void loadApns(SQLiteDatabase db, XmlPullParser parser) {
+            if (parser != null) {
+                try {
+                    while (true) {
+                        XmlUtils.nextElement(parser);
+                        ContentValues row = getRow(parser);
+                        if (row != null) {
+                            db.insert(CARRIERS_TABLE, null, row);
+                        } else {
+                            break;  // do we really want to skip the rest of the file?
+                        }
+                    }
+                } catch (XmlPullParserException e)  {
+                    Log.e(TAG, "Got execption while getting perferred time zone.", e);
+                } catch (IOException e) {
+                    Log.e(TAG, "Got execption while getting perferred time zone.", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new DatabaseHelper(getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri url, String[] projectionIn, String selection,
+            String[] selectionArgs, String sort) {
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables("carriers");
+
+        int match = s_urlMatcher.match(url);
+        switch (match) {
+            // do nothing
+            case URL_TELEPHONY: {
+                break;
+            }
+
+
+            case URL_CURRENT: {
+                qb.appendWhere("current IS NOT NULL");
+                break;
+            }
+
+            case URL_ID: {
+                qb.appendWhere("_id = " + url.getPathSegments().get(1));
+                break;
+            }
+
+            default: {
+                return null;
+            }
+        }
+
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor ret = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort);
+        ret.setNotificationUri(getContext().getContentResolver(), url);
+        return ret;
+    }
+
+    @Override
+    public String getType(Uri url)
+    {
+        switch (s_urlMatcher.match(url)) {
+        case URL_TELEPHONY:
+            return "vnd.android.cursor.dir/telephony-carrier";
+
+        case URL_ID:
+            return "vnd.android.cursor.item/telephony-carrier";
+
+        default:
+            throw new IllegalArgumentException("Unknown URL " + url);
+        }
+    }
+
+    @Override
+    public Uri insert(Uri url, ContentValues initialValues)
+    {
+        Uri result = null;
+
+        checkPermission();
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int match = s_urlMatcher.match(url);
+        boolean notify = false;
+        switch (match)
+        {
+            case URL_TELEPHONY:
+            {
+                ContentValues values;
+                if (initialValues != null) {
+                    values = new ContentValues(initialValues);
+                } else {
+                    values = new ContentValues();
+                }
+
+                // TODO Review this. This code should probably not bet here.
+                // It is valid for the database to return a null string.
+                if (values.containsKey(Telephony.Carriers.NAME) == false) {
+                    values.put(Telephony.Carriers.NAME, "");
+                }
+                if (values.containsKey(Telephony.Carriers.APN) == false) {
+                    values.put(Telephony.Carriers.APN, "");
+                }
+                if (values.containsKey(Telephony.Carriers.PORT) == false) {
+                    values.put(Telephony.Carriers.PORT, "");
+                }
+                if (values.containsKey(Telephony.Carriers.PROXY) == false) {
+                    values.put(Telephony.Carriers.PROXY, "");
+                }
+                if (values.containsKey(Telephony.Carriers.USER) == false) {
+                    values.put(Telephony.Carriers.USER, "");
+                }
+                if (values.containsKey(Telephony.Carriers.SERVER) == false) {
+                    values.put(Telephony.Carriers.SERVER, "");
+                }
+                if (values.containsKey(Telephony.Carriers.PASSWORD) == false) {
+                    values.put(Telephony.Carriers.PASSWORD, "");
+                }
+                if (values.containsKey(Telephony.Carriers.MMSPORT) == false) {
+                    values.put(Telephony.Carriers.MMSPORT, "");
+                }
+                if (values.containsKey(Telephony.Carriers.MMSPROXY) == false) {
+                    values.put(Telephony.Carriers.MMSPROXY, "");
+                }
+
+                long rowID = db.insert(CARRIERS_TABLE, null, values);
+                if (rowID > 0)
+                {
+                    result = ContentUris.withAppendedId(Telephony.Carriers.CONTENT_URI, rowID);
+                    notify = true;
+                }
+
+                if (Config.LOGD) Log.d(TAG, "inserted " + values.toString() + " rowID = " + rowID);
+                break;
+            }
+
+            case URL_CURRENT:
+            {
+                // null out the previous operator
+                db.update("carriers", s_currentNullMap, "current IS NOT NULL", null);
+
+                String numeric = initialValues.getAsString("numeric");
+                int updated = db.update("carriers", s_currentSetMap,
+                        "numeric = '" + numeric + "'", null);
+
+                if (updated > 0)
+                {
+                    if (Config.LOGD) {
+                        Log.d(TAG, "Setting numeric '" + numeric + "' to be the current operator");
+                    }
+                }
+                else
+                {
+                    Log.e(TAG, "Failed setting numeric '" + numeric + "' to the current operator");
+                }
+                break;
+            }
+        }
+
+        if (notify) {
+            getContext().getContentResolver().notifyChange(Telephony.Carriers.CONTENT_URI, null);
+        }
+
+        return result;
+    }
+
+    @Override
+    public int delete(Uri url, String where, String[] whereArgs)
+    {
+        int count;
+
+        checkPermission();
+        
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int match = s_urlMatcher.match(url);
+        switch (match)
+        {
+            case URL_TELEPHONY:
+            {
+                count = db.delete(CARRIERS_TABLE, where, whereArgs);
+                break;
+            }
+
+            case URL_CURRENT:
+            {
+                count = db.delete(CARRIERS_TABLE, where, whereArgs);
+                break;
+            }
+            
+            case URL_ID:
+            {
+                count = db.delete(CARRIERS_TABLE, Telephony.Carriers._ID + "=?",
+                        new String[] { url.getLastPathSegment() });
+                break;
+            }
+
+            case URL_RESTOREAPN: {
+                count = 1;
+                restoreDefaultAPN();
+                break;
+            }
+
+            default: {
+                throw new UnsupportedOperationException("Cannot delete that URL: " + url);
+            }
+        }
+
+        if (count > 0) {
+            getContext().getContentResolver().notifyChange(Telephony.Carriers.CONTENT_URI, null);
+        }
+
+        return count;
+    }
+
+    @Override
+    public int update(Uri url, ContentValues values, String where, String[] whereArgs)
+    {
+        int count;
+
+        checkPermission();
+
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int match = s_urlMatcher.match(url);
+        switch (match)
+        {
+            case URL_TELEPHONY:
+            {
+                count = db.update(CARRIERS_TABLE, values, where, whereArgs);
+                break;
+            }
+
+            case URL_CURRENT:
+            {
+                count = db.update(CARRIERS_TABLE, values, where, whereArgs);
+                break;
+            }
+
+            case URL_ID:
+            {
+                if (where != null || whereArgs != null) {
+                    throw new UnsupportedOperationException(
+                            "Cannot update URL " + url + " with a where clause");
+                }
+                count = db.update(CARRIERS_TABLE, values, Telephony.Carriers._ID + "=?",
+                        new String[] { url.getLastPathSegment() });
+                break;
+            }
+
+            default: {
+                throw new UnsupportedOperationException("Cannot update that URL: " + url);
+            }
+        }
+
+        if (count > 0) {
+            getContext().getContentResolver().notifyChange(Telephony.Carriers.CONTENT_URI, null);
+        }
+
+        return count;
+    }
+
+    private void checkPermission() {
+        // Check the permissions
+        getContext().enforceCallingOrSelfPermission("android.permission.WRITE_APN_SETTINGS",
+                "No permission to write APN settings");
+    }
+
+    private SQLiteOpenHelper mOpenHelper;
+
+    private void restoreDefaultAPN() {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+        db.delete(CARRIERS_TABLE, null, null);
+        ((DatabaseHelper) mOpenHelper).initDatabase(db);
+    }
+}