Bug 674725 - Part AV - Port WebSMS to Android Native UI. r=blassey,dougt
authorMounir Lamouri <mounir.lamouri@gmail.com>
Tue, 17 Jan 2012 19:52:17 +0100
changeset 85958 ba43b80683e70093ad282833f391932775cc2799
parent 85957 6c91add316dda5249d975327d4f99d85d7a45df8
child 85959 544fd25dfc6121e70ecfbca52a87a6a9b34d1b41
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersblassey, dougt
bugs674725
milestone12.0a1
Bug 674725 - Part AV - Port WebSMS to Android Native UI. r=blassey,dougt
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoAppShell.java
mobile/android/base/GeckoSmsManager.java
mobile/android/installer/package-manifest.in
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -19,16 +19,22 @@
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
     <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
 
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
     <uses-permission android:name="android.permission.VIBRATE"/>
 
+    <!-- WebSMS -->
+    <uses-permission android:name="android.permission.SEND_SMS"/>
+    <uses-permission android:name="android.permission.RECEIVE_SMS"/>
+    <uses-permission android:name="android.permission.WRITE_SMS"/>
+    <uses-permission android:name="android.permission.READ_SMS"/>
+
     <uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/> 
     <uses-permission android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"/> 
 
     <uses-feature android:name="android.hardware.location" android:required="false"/>
     <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
     <uses-feature android:name="android.hardware.touchscreen"/>
 
     <uses-permission android:name="android.permission.CAMERA" />
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1567,20 +1567,24 @@ abstract public class GeckoApp
         mConnectivityReceiver = new GeckoConnectivityReceiver();
 
         IntentFilter batteryFilter = new IntentFilter();
         batteryFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
         mBatteryReceiver = new GeckoBatteryManager();
         registerReceiver(mBatteryReceiver, batteryFilter);
 
         IntentFilter smsFilter = new IntentFilter();
-        smsFilter.addAction("android.provider.Telephony.SMS_RECEIVED");
+        smsFilter.addAction(GeckoSmsManager.ACTION_SMS_RECEIVED);
+        smsFilter.addAction(GeckoSmsManager.ACTION_SMS_SENT);
+        smsFilter.addAction(GeckoSmsManager.ACTION_SMS_DELIVERED);
         mSmsReceiver = new GeckoSmsManager();
         registerReceiver(mSmsReceiver, smsFilter);
 
+        GeckoSmsManager.init();
+
         final GeckoApp self = this;
  
         GeckoAppShell.getHandler().postDelayed(new Runnable() {
             public void run() {
                 
                 Log.w(LOGTAG, "zerdatime " + new Date().getTime() + " - pre checkLaunchState");
 
                 /*
@@ -1795,16 +1799,18 @@ abstract public class GeckoApp
         GeckoAppShell.unregisterGeckoEventListener("ToggleChrome:Hide", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("ToggleChrome:Show", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("FormAssist:AutoComplete", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Permissions:Data", GeckoApp.mAppContext);
         GeckoAppShell.unregisterGeckoEventListener("Downloads:Done", GeckoApp.mAppContext);
 
         mFavicons.close();
 
+        GeckoSmsManager.shutdown();
+
         super.onDestroy();
 
         unregisterReceiver(mSmsReceiver);
         unregisterReceiver(mBatteryReceiver);
     }
 
     @Override
     public void onContentChanged() {
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -136,16 +136,29 @@ public class GeckoAppShell
         sendEventToGecko(new GeckoEvent(GeckoEvent.VISTITED, uri));
     }
 
     public static native void processNextNativeEvent();
 
     public static native void notifyBatteryChange(double aLevel, boolean aCharging, double aRemainingTime);
 
     public static native void notifySmsReceived(String aSender, String aBody, long aTimestamp);
+    public static native int  saveMessageInSentbox(String aReceiver, String aBody, long aTimestamp);
+    public static native void notifySmsSent(int aId, String aReceiver, String aBody, long aTimestamp, int aRequestId, long aProcessId);
+    public static native void notifySmsDelivered(int aId, String aReceiver, String aBody, long aTimestamp);
+    public static native void notifySmsSendFailed(int aError, int aRequestId, long aProcessId);
+    public static native void notifyGetSms(int aId, String aReceiver, String aSender, String aBody, long aTimestamp, int aRequestId, long aProcessId);
+    public static native void notifyGetSmsFailed(int aError, int aRequestId, long aProcessId);
+    public static native void notifySmsDeleted(boolean aDeleted, int aRequestId, long aProcessId);
+    public static native void notifySmsDeleteFailed(int aError, int aRequestId, long aProcessId);
+    public static native void notifyNoMessageInList(int aRequestId, long aProcessId);
+    public static native void notifyListCreated(int aListId, int aMessageId, String aReceiver, String aSender, String aBody, long aTimestamp, int aRequestId, long aProcessId);
+    public static native void notifyGotNextMessage(int aMessageId, String aReceiver, String aSender, String aBody, long aTimestamp, int aRequestId, long aProcessId);
+    public static native void notifyReadingMessageListFailed(int aError, int aRequestId, long aProcessId);
+
     public static native ByteBuffer allocateDirectBuffer(long size);
     public static native void freeDirectBuffer(ByteBuffer buf);
     public static native void bindWidgetTexture();
     public static native boolean testDirectTexture();
 
     // A looper thread, accessed by GeckoAppShell.getHandler
     private static class LooperThread extends Thread {
         public SynchronousQueue<Handler> mHandlerQueue =
@@ -1629,18 +1642,42 @@ public class GeckoAppShell
 
     /*
      * WebSMS related methods.
      */
     public static int getNumberOfMessagesForText(String aText) {
         return GeckoSmsManager.getNumberOfMessagesForText(aText);
     }
 
-    public static void sendMessage(String aNumber, String aMessage) {
-        GeckoSmsManager.send(aNumber, aMessage);
+    public static void sendMessage(String aNumber, String aMessage, int aRequestId, long aProcessId) {
+        GeckoSmsManager.send(aNumber, aMessage, aRequestId, aProcessId);
+    }
+
+    public static int saveSentMessage(String aRecipient, String aBody, long aDate) {
+        return GeckoSmsManager.saveSentMessage(aRecipient, aBody, aDate);
+    }
+
+    public static void getMessage(int aMessageId, int aRequestId, long aProcessId) {
+        GeckoSmsManager.getMessage(aMessageId, aRequestId, aProcessId);
+    }
+
+    public static void deleteMessage(int aMessageId, int aRequestId, long aProcessId) {
+        GeckoSmsManager.deleteMessage(aMessageId, aRequestId, aProcessId);
+    }
+
+    public static void createMessageList(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, int aDeliveryState, boolean aReverse, int aRequestId, long aProcessId) {
+        GeckoSmsManager.createMessageList(aStartDate, aEndDate, aNumbers, aNumbersCount, aDeliveryState, aReverse, aRequestId, aProcessId);
+    }
+
+    public static void getNextMessageInList(int aListId, int aRequestId, long aProcessId) {
+        GeckoSmsManager.getNextMessageInList(aListId, aRequestId, aProcessId);
+    }
+
+    public static void clearMessageList(int aListId) {
+        GeckoSmsManager.clearMessageList(aListId);
     }
 
     public static boolean isTablet() {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
             Configuration config = GeckoApp.mAppContext.getResources().getConfiguration();
             // xlarge is defined by android as screens larger than 960dp x 720dp
             // and should include most devices ~7in and up.
             // http://developer.android.com/guide/practices/screens_support.html
--- a/mobile/android/base/GeckoSmsManager.java
+++ b/mobile/android/base/GeckoSmsManager.java
@@ -33,36 +33,339 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko;
 
 import java.util.ArrayList;
+import java.util.Iterator;
 
 import android.util.Log;
 
+import android.app.PendingIntent;
+import android.app.Activity;
+
+import android.database.Cursor;
+
 import android.content.Intent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.ContentUris;
+
+import android.net.Uri;
 
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 
 import android.telephony.SmsManager;
 import android.telephony.SmsMessage;
 
+/**
+ * This class is returning unique ids for PendingIntent requestCode attribute.
+ * There are only |Integer.MAX_VALUE - Integer.MIN_VALUE| unique IDs available,
+ * and they wrap around.
+ */
+class PendingIntentUID
+{
+  static private int sUID = Integer.MIN_VALUE;
+
+  static public int generate() { return sUID++; }
+}
+
+/**
+ * The envelope class contains all information that are needed to keep track of
+ * a sent SMS.
+ */
+class Envelope
+{
+  enum SubParts {
+    SENT_PART,
+    DELIVERED_PART
+  }
+
+  protected int       mId;
+  protected int       mMessageId;
+  protected long      mMessageTimestamp;
+
+  /**
+   * Number of sent/delivered remaining parts.
+   * @note The array has much slots as SubParts items.
+   */
+  protected int[]     mRemainingParts;
+
+  /**
+   * Whether sending/delivering is currently failing.
+   * @note The array has much slots as SubParts items.
+   */
+  protected boolean[] mFailing;
+
+  /**
+   * Error type (only for sent).
+   */
+  protected int       mError;
+
+  public Envelope(int aId, int aParts) {
+    mId = aId;
+    mMessageId = -1;
+    mMessageTimestamp = 0;
+    mError = GeckoSmsManager.kNoError;
+
+    int size = Envelope.SubParts.values().length;
+    mRemainingParts = new int[size];
+    mFailing = new boolean[size];
+
+    for (int i=0; i<size; ++i) {
+      mRemainingParts[i] = aParts;
+      mFailing[i] = false;
+    }
+  }
+
+  public void decreaseRemainingParts(Envelope.SubParts aType) {
+    --mRemainingParts[aType.ordinal()];
+
+    if (mRemainingParts[SubParts.SENT_PART.ordinal()] >
+        mRemainingParts[SubParts.DELIVERED_PART.ordinal()]) {
+      Log.e("GeckoSmsManager", "Delivered more parts than we sent!?");
+    }
+  }
+
+  public boolean arePartsRemaining(Envelope.SubParts aType) {
+    return mRemainingParts[aType.ordinal()] != 0;
+  }
+
+  public void markAsFailed(Envelope.SubParts aType) {
+    mFailing[aType.ordinal()] = true;
+  }
+
+  public boolean isFailing(Envelope.SubParts aType) {
+    return mFailing[aType.ordinal()];
+  }
+
+  public int getMessageId() {
+    return mMessageId;
+  }
+
+  public void setMessageId(int aMessageId) {
+    mMessageId = aMessageId;
+  }
+
+  public long getMessageTimestamp() {
+    return mMessageTimestamp;
+  }
+
+  public void setMessageTimestamp(long aMessageTimestamp) {
+    mMessageTimestamp = aMessageTimestamp;
+  }
+
+  public int getError() {
+    return mError;
+  }
+
+  public void setError(int aError) {
+    mError = aError;
+  }
+}
+
+/**
+ * Postman class is a singleton that manages Envelope instances.
+ */
+class Postman
+{
+  public static final int kUnknownEnvelopeId = -1;
+
+  private static final Postman sInstance = new Postman();
+
+  private ArrayList<Envelope> mEnvelopes = new ArrayList<Envelope>(1);
+
+  private Postman() {}
+
+  public static Postman getInstance() {
+    return sInstance;
+  }
+
+  public int createEnvelope(int aParts) {
+    /*
+     * We are going to create the envelope in the first empty slot in the array
+     * list. If there is no empty slot, we create a new one.
+     */
+    int size = mEnvelopes.size();
+
+    for (int i=0; i<size; ++i) {
+      if (mEnvelopes.get(i) == null) {
+        mEnvelopes.set(i, new Envelope(i, aParts));
+        return i;
+      }
+    }
+
+    mEnvelopes.add(new Envelope(size, aParts));
+    return size;
+  }
+
+  public Envelope getEnvelope(int aId) {
+    if (aId < 0 || mEnvelopes.size() <= aId) {
+      Log.e("GeckoSmsManager", "Trying to get an unknown Envelope!");
+      return null;
+    }
+
+    Envelope envelope = mEnvelopes.get(aId);
+    if (envelope == null) {
+      Log.e("GeckoSmsManager", "Trying to get an empty Envelope!");
+    }
+
+    return envelope;
+  }
+
+  public void destroyEnvelope(int aId) {
+    if (aId < 0 || mEnvelopes.size() <= aId) {
+      Log.e("GeckoSmsManager", "Trying to destroy an unknown Envelope!");
+      return;
+    }
+
+    if (mEnvelopes.set(aId, null) == null) {
+      Log.e("GeckoSmsManager", "Trying to destroy an empty Envelope!");
+    }
+  }
+}
+
+class SmsIOThread extends Thread {
+  private final static SmsIOThread sInstance = new SmsIOThread();
+
+  private Handler mHandler;
+
+  public static SmsIOThread getInstance() {
+    return sInstance;
+  }
+
+  public boolean execute(Runnable r) {
+    return mHandler.post(r);
+  }
+
+  public void run() {
+    Looper.prepare();
+
+    mHandler = new Handler();
+
+    Looper.loop();
+  }
+}
+
+class MessagesListManager
+{
+  private static final MessagesListManager sInstance = new MessagesListManager();
+
+  public static MessagesListManager getInstance() {
+    return sInstance;
+  }
+
+  private ArrayList<Cursor> mCursors = new ArrayList<Cursor>(0);
+
+  public int add(Cursor aCursor) {
+    int size = mCursors.size();
+
+    for (int i=0; i<size; ++i) {
+      if (mCursors.get(i) == null) {
+        mCursors.set(i, aCursor);
+        return i;
+      }
+    }
+
+    mCursors.add(aCursor);
+    return size;
+  }
+
+  public Cursor get(int aId) {
+    if (aId < 0 || mCursors.size() <= aId) {
+      Log.e("GeckoSmsManager", "Trying to get an unknown list!");
+      return null;
+    }
+
+    Cursor cursor = mCursors.get(aId);
+    if (cursor == null) {
+      Log.e("GeckoSmsManager", "Trying to get an empty list!");
+    }
+
+    return cursor;
+  }
+
+  public void remove(int aId) {
+    if (aId < 0 || mCursors.size() <= aId) {
+      Log.e("GeckoSmsManager", "Trying to destroy an unknown list!");
+      return;
+    }
+
+    Cursor cursor = mCursors.set(aId, null);
+    if (cursor == null) {
+      Log.e("GeckoSmsManager", "Trying to destroy an empty list!");
+      return;
+    }
+
+    cursor.close();
+  }
+
+  public void clear() {
+    for (int i=0; i<mCursors.size(); ++i) {
+      Cursor c = mCursors.get(i);
+      if (c != null) {
+        c.close();
+      }
+    }
+
+    mCursors.clear();
+  }
+}
+
 public class GeckoSmsManager
   extends BroadcastReceiver
 {
-  final static int kMaxMessageSize = 160;
+  public final static String ACTION_SMS_RECEIVED  = "android.provider.Telephony.SMS_RECEIVED";
+  public final static String ACTION_SMS_SENT      = "org.mozilla.gecko.SMS_SENT";
+  public final static String ACTION_SMS_DELIVERED = "org.mozilla.gecko.SMS_DELIVERED";
+
+  /*
+   * Make sure that the following error codes are in sync with |ErrorType| in:
+   * dom/sms/src/Types.h
+   * The error code are owned by the DOM.
+   */
+  public final static int kNoError       = 0;
+  public final static int kNoSignalError = 1;
+  public final static int kNotFoundError = 2;
+  public final static int kUnknownError  = 3;
+  public final static int kInternalError = 4;
+
+  private final static int kMaxMessageSize    = 160;
+
+  private final static Uri kSmsContentUri     = Uri.parse("content://sms");
+  private final static Uri kSmsSentContentUri = Uri.parse("content://sms/sent");
+
+  private final static int kSmsTypeInbox      = 1;
+  private final static int kSmsTypeSentbox    = 2;
+
+  /*
+   * Keep the following error codes in syng with |DeliveryState| in:
+   * dom/sms/src/Types.h
+   */
+  private final static int kDeliveryStateSent     = 0;
+  private final static int kDeliveryStateReceived = 1;
+  private final static int kDeliveryStateUnknown  = 2;
+  private final static int kDeliveryStateEndGuard = 3;
+
+  private final static String[] kRequiredMessageRows = new String[] { "_id", "address", "body", "date", "type" };
+
+  public static void init() {
+    SmsIOThread.getInstance().start();
+  }
 
   @Override
   public void onReceive(Context context, Intent intent) {
-    if (intent.getAction().equals("android.provider.Telephony.SMS_RECEIVED")) {
+    if (intent.getAction().equals(ACTION_SMS_RECEIVED)) {
       // TODO: Try to find the receiver number to be able to populate
       //       SmsMessage.receiver.
       // TODO: Get the id and the date from the stock app saved message.
       //       Using the stock app saved message require us to wait for it to
       //       be saved which can lead to race conditions.
 
       Bundle bundle = intent.getExtras();
 
@@ -74,35 +377,547 @@ public class GeckoSmsManager
 
       for (int i=0; i<pdus.length; ++i) {
         SmsMessage msg = SmsMessage.createFromPdu((byte[])pdus[i]);
 
         GeckoAppShell.notifySmsReceived(msg.getDisplayOriginatingAddress(),
                                         msg.getDisplayMessageBody(),
                                         System.currentTimeMillis());
       }
+
+      return;
+    }
+
+    if (intent.getAction().equals(ACTION_SMS_SENT) ||
+        intent.getAction().equals(ACTION_SMS_DELIVERED)) {
+      Bundle bundle = intent.getExtras();
+
+      if (bundle == null || !bundle.containsKey("envelopeId") ||
+          !bundle.containsKey("number") || !bundle.containsKey("message") ||
+          !bundle.containsKey("requestId") || !bundle.containsKey("processId")) {
+        Log.e("GeckoSmsManager", "Got an invalid ACTION_SMS_SENT/ACTION_SMS_DELIVERED!");
+        return;
+      }
+
+      int envelopeId = bundle.getInt("envelopeId");
+      Postman postman = Postman.getInstance();
+
+      Envelope envelope = postman.getEnvelope(envelopeId);
+      if (envelope == null) {
+        Log.e("GeckoSmsManager", "Got an invalid envelope id (or Envelope has been destroyed)!");
+        return;
+      }
+
+      Envelope.SubParts part = intent.getAction().equals(ACTION_SMS_SENT)
+                                 ? Envelope.SubParts.SENT_PART
+                                 : Envelope.SubParts.DELIVERED_PART;
+      envelope.decreaseRemainingParts(part);
+ 
+
+      if (getResultCode() != Activity.RESULT_OK) {
+        switch (getResultCode()) {
+          case SmsManager.RESULT_ERROR_NULL_PDU:
+            envelope.setError(kInternalError);
+            break;
+          case SmsManager.RESULT_ERROR_NO_SERVICE:
+          case SmsManager.RESULT_ERROR_RADIO_OFF:
+            envelope.setError(kNoSignalError);
+            break;
+          case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+          default:
+            envelope.setError(kUnknownError);
+            break;
+        }
+        envelope.markAsFailed(part);
+        Log.i("GeckoSmsManager", "SMS part sending failed!");
+      }
+
+      if (envelope.arePartsRemaining(part)) {
+        return;
+      }
+
+      if (envelope.isFailing(part)) {
+        if (part == Envelope.SubParts.SENT_PART) {
+          GeckoAppShell.notifySmsSendFailed(envelope.getError(),
+                                            bundle.getInt("requestId"),
+                                            bundle.getLong("processId"));
+          Log.i("GeckoSmsManager", "SMS sending failed!");
+        } else {
+          // It seems unlikely to get a result code for a failure to deliver.
+          // Even if, we don't want to do anything with this.
+          Log.e("GeckoSmsManager", "SMS failed to be delivered... is that even possible?");
+        }
+      } else {
+        if (part == Envelope.SubParts.SENT_PART) {
+          String number = bundle.getString("number");
+          String message = bundle.getString("message");
+          long timestamp = System.currentTimeMillis();
+
+          int id = GeckoAppShell.saveMessageInSentbox(number, message, timestamp);
+
+          GeckoAppShell.notifySmsSent(id, number, message, timestamp,
+                                      bundle.getInt("requestId"),
+                                      bundle.getLong("processId"));
+
+          envelope.setMessageId(id);
+          envelope.setMessageTimestamp(timestamp);
+
+          Log.i("GeckoSmsManager", "SMS sending was successfull!");
+        } else {
+          GeckoAppShell.notifySmsDelivered(envelope.getMessageId(),
+                                           bundle.getString("number"),
+                                           bundle.getString("message"),
+                                           envelope.getMessageTimestamp());
+          Log.i("GeckoSmsManager", "SMS succesfully delivered!");
+        }
+      }
+
+      // Destroy the envelope object only if the SMS has been sent and delivered.
+      if (!envelope.arePartsRemaining(Envelope.SubParts.SENT_PART) &&
+          !envelope.arePartsRemaining(Envelope.SubParts.DELIVERED_PART)) {
+        postman.destroyEnvelope(envelopeId);
+      }
+
+      return;
     }
   }
 
   public static int getNumberOfMessagesForText(String aText) {
     return SmsManager.getDefault().divideMessage(aText).size();
   }
 
-  public static void send(String aNumber, String aMessage) {
-    /*
-     * TODO:
-     * This is a basic send method that doesn't handle errors, doesn't listen to
-     * sent and received messages. It's only calling the send method.
-     */
+  public static void send(String aNumber, String aMessage, int aRequestId, long aProcessId) {
+    int envelopeId = Postman.kUnknownEnvelopeId;
+
     try {
       SmsManager sm = SmsManager.getDefault();
 
+      Intent sentIntent = new Intent(ACTION_SMS_SENT);
+      Intent deliveredIntent = new Intent(ACTION_SMS_DELIVERED);
+
+      Bundle bundle = new Bundle();
+      bundle.putString("number", aNumber);
+      bundle.putString("message", aMessage);
+      bundle.putInt("requestId", aRequestId);
+      bundle.putLong("processId", aProcessId);
+
       if (aMessage.length() <= kMaxMessageSize) {
-        sm.sendTextMessage(aNumber, "", aMessage, null, null);
+        envelopeId = Postman.getInstance().createEnvelope(1);
+        bundle.putInt("envelopeId", envelopeId);
+
+        sentIntent.putExtras(bundle);
+        deliveredIntent.putExtras(bundle);
+
+        /*
+         * There are a few things to know about getBroadcast and pending intents:
+         * - the pending intents are in a shared pool maintained by the system;
+         * - each pending intent is identified by a token;
+         * - when a new pending intent is created, if it has the same token as
+         *   another intent in the pool, one of them has to be removed.
+         *
+         * To prevent having a hard time because of this situation, we give a
+         * unique id to all pending intents we are creating. This unique id is
+         * generated by GetPendingIntentUID().
+         */
+        PendingIntent sentPendingIntent =
+          PendingIntent.getBroadcast(GeckoApp.mAppContext,
+                                     PendingIntentUID.generate(), sentIntent,
+                                     PendingIntent.FLAG_CANCEL_CURRENT);
+
+        PendingIntent deliveredPendingIntent =
+          PendingIntent.getBroadcast(GeckoApp.mAppContext,
+                                     PendingIntentUID.generate(), deliveredIntent,
+                                     PendingIntent.FLAG_CANCEL_CURRENT);
+
+        sm.sendTextMessage(aNumber, "", aMessage,
+                           sentPendingIntent, deliveredPendingIntent);
       } else {
         ArrayList<String> parts = sm.divideMessage(aMessage);
-        sm.sendMultipartTextMessage(aNumber, "", parts, null, null);
+        envelopeId = Postman.getInstance().createEnvelope(parts.size());
+        bundle.putInt("envelopeId", envelopeId);
+
+        sentIntent.putExtras(bundle);
+        deliveredIntent.putExtras(bundle);
+
+        ArrayList<PendingIntent> sentPendingIntents =
+          new ArrayList<PendingIntent>(parts.size());
+        ArrayList<PendingIntent> deliveredPendingIntents =
+          new ArrayList<PendingIntent>(parts.size());
+
+        for (int i=0; i<parts.size(); ++i) {
+          sentPendingIntents.add(
+            PendingIntent.getBroadcast(GeckoApp.mAppContext,
+                                       PendingIntentUID.generate(), sentIntent,
+                                       PendingIntent.FLAG_CANCEL_CURRENT)
+          );
+
+          deliveredPendingIntents.add(
+            PendingIntent.getBroadcast(GeckoApp.mAppContext,
+                                       PendingIntentUID.generate(), deliveredIntent,
+                                       PendingIntent.FLAG_CANCEL_CURRENT)
+          );
+        }
+
+        sm.sendMultipartTextMessage(aNumber, "", parts, sentPendingIntents,
+                                    deliveredPendingIntents);
       }
     } catch (Exception e) {
-      Log.i("GeckoSmsManager", "Failed to send an SMS: ", e);
+      Log.e("GeckoSmsManager", "Failed to send an SMS: ", e);
+
+      if (envelopeId != Postman.kUnknownEnvelopeId) {
+        Postman.getInstance().destroyEnvelope(envelopeId);
+      }
+
+      GeckoAppShell.notifySmsSendFailed(kUnknownError, aRequestId, aProcessId);
+    }
+  }
+
+  public static int saveSentMessage(String aRecipient, String aBody, long aDate) {
+    class IdTooHighException extends Exception { }
+
+    try {
+      ContentValues values = new ContentValues();
+      values.put("address", aRecipient);
+      values.put("body", aBody);
+      values.put("date", aDate);
+
+      ContentResolver cr = GeckoApp.mAppContext.getContentResolver();
+      Uri uri = cr.insert(kSmsSentContentUri, values);
+
+      long id = ContentUris.parseId(uri);
+
+      // The DOM API takes a 32bits unsigned int for the id. It's unlikely that
+      // we happen to need more than that but it doesn't cost to check.
+      if (id > Integer.MAX_VALUE) {
+        throw new IdTooHighException();
+      }
+
+      return (int)id;
+    } catch (IdTooHighException e) {
+      Log.e("GeckoSmsManager", "The id we received is higher than the higher allowed value.");
+      return -1;
+    } catch (Exception e) {
+      Log.e("GeckoSmsManager", "Something went wrong when trying to write a sent message: " + e);
+      return -1;
+    }
+  }
+
+  public static void getMessage(int aMessageId, int aRequestId, long aProcessId) {
+    class GetMessageRunnable implements Runnable {
+      private int mMessageId;
+      private int mRequestId;
+      private long mProcessId;
+
+      GetMessageRunnable(int aMessageId, int aRequestId, long aProcessId) {
+        mMessageId = aMessageId;
+        mRequestId = aRequestId;
+        mProcessId = aProcessId;
+      }
+
+      @Override
+      public void run() {
+        class NotFoundException extends Exception { }
+        class UnmatchingIdException extends Exception { }
+        class TooManyResultsException extends Exception { }
+        class InvalidTypeException extends Exception { }
+
+        Cursor cursor = null;
+
+        try {
+          ContentResolver cr = GeckoApp.mAppContext.getContentResolver();
+          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+          cursor = cr.query(message, kRequiredMessageRows, null, null, null);
+          if (cursor == null || cursor.getCount() == 0) {
+            throw new NotFoundException();
+          }
+
+          if (cursor.getCount() != 1) {
+            throw new TooManyResultsException();
+          }
+
+          cursor.moveToFirst();
+
+          if (cursor.getInt(cursor.getColumnIndex("_id")) != mMessageId) {
+            throw new UnmatchingIdException();
+          }
+
+          int type = cursor.getInt(cursor.getColumnIndex("type"));
+          String sender = "";
+          String receiver = "";
+
+          if (type == kSmsTypeInbox) {
+            sender = cursor.getString(cursor.getColumnIndex("address"));
+          } else if (type == kSmsTypeSentbox) {
+            receiver = cursor.getString(cursor.getColumnIndex("address"));
+          } else {
+            throw new InvalidTypeException();
+          }
+
+          GeckoAppShell.notifyGetSms(cursor.getInt(cursor.getColumnIndex("_id")),
+                                     receiver, sender,
+                                     cursor.getString(cursor.getColumnIndex("body")),
+                                     cursor.getLong(cursor.getColumnIndex("date")),
+                                     mRequestId, mProcessId);
+        } catch (NotFoundException e) {
+          Log.i("GeckoSmsManager", "Message id " + mMessageId + " not found");
+          GeckoAppShell.notifyGetSmsFailed(kNotFoundError, mRequestId, mProcessId);
+        } catch (UnmatchingIdException e) {
+          Log.e("GeckoSmsManager", "Requested message id (" + mMessageId +
+                                   ") is different from the one we got.");
+          GeckoAppShell.notifyGetSmsFailed(kUnknownError, mRequestId, mProcessId);
+        } catch (TooManyResultsException e) {
+          Log.e("GeckoSmsManager", "Get too many results for id " + mMessageId);
+          GeckoAppShell.notifyGetSmsFailed(kUnknownError, mRequestId, mProcessId);
+        } catch (InvalidTypeException e) {
+          Log.i("GeckoSmsManager", "Message has an invalid type, we ignore it.");
+          GeckoAppShell.notifyGetSmsFailed(kNotFoundError, mRequestId, mProcessId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to get message: " + e);
+          GeckoAppShell.notifyGetSmsFailed(kUnknownError, mRequestId, mProcessId);
+        } finally {
+          if (cursor != null) {
+            cursor.close();
+          }
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new GetMessageRunnable(aMessageId, aRequestId, aProcessId))) {
+      Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
+      GeckoAppShell.notifyGetSmsFailed(kUnknownError, aRequestId, aProcessId);
+    }
+  }
+
+  public static void deleteMessage(int aMessageId, int aRequestId, long aProcessId) {
+    class DeleteMessageRunnable implements Runnable {
+      private int mMessageId;
+      private int mRequestId;
+      private long mProcessId;
+
+      DeleteMessageRunnable(int aMessageId, int aRequestId, long aProcessId) {
+        mMessageId = aMessageId;
+        mRequestId = aRequestId;
+        mProcessId = aProcessId;
+      }
+
+      @Override
+      public void run() {
+        class TooManyResultsException extends Exception { }
+
+        try {
+          ContentResolver cr = GeckoApp.mAppContext.getContentResolver();
+          Uri message = ContentUris.withAppendedId(kSmsContentUri, mMessageId);
+
+          int count = cr.delete(message, null, null);
+
+          if (count > 1) {
+            throw new TooManyResultsException();
+          }
+
+          GeckoAppShell.notifySmsDeleted(count == 1, mRequestId, mProcessId);
+        } catch (TooManyResultsException e) {
+          Log.e("GeckoSmsManager", "Delete more than one message? " + e);
+          GeckoAppShell.notifySmsDeleteFailed(kUnknownError, mRequestId, mProcessId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to delete a message: " + e);
+          GeckoAppShell.notifySmsDeleteFailed(kUnknownError, mRequestId, mProcessId);
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new DeleteMessageRunnable(aMessageId, aRequestId, aProcessId))) {
+      Log.e("GeckoSmsManager", "Failed to add GetMessageRunnable to the SmsIOThread");
+      GeckoAppShell.notifySmsDeleteFailed(kUnknownError, aRequestId, aProcessId);
     }
   }
+
+  public static void createMessageList(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, int aDeliveryState, boolean aReverse, int aRequestId, long aProcessId) {
+    class CreateMessageListRunnable implements Runnable {
+      private long     mStartDate;
+      private long     mEndDate;
+      private String[] mNumbers;
+      private int      mNumbersCount;
+      private int      mDeliveryState;
+      private boolean  mReverse;
+      private int      mRequestId;
+      private long     mProcessId;
+
+      CreateMessageListRunnable(long aStartDate, long aEndDate, String[] aNumbers, int aNumbersCount, int aDeliveryState, boolean aReverse, int aRequestId, long aProcessId) {
+        mStartDate = aStartDate;
+        mEndDate = aEndDate;
+        mNumbers = aNumbers;
+        mNumbersCount = aNumbersCount;
+        mDeliveryState = aDeliveryState;
+        mReverse = aReverse;
+        mRequestId = aRequestId;
+        mProcessId = aProcessId;
+      }
+
+      @Override
+      public void run() {
+        class UnexpectedDeliveryStateException extends Exception { };
+        class InvalidTypeException extends Exception { }
+
+        Cursor cursor = null;
+        boolean closeCursor = true;
+
+        try {
+          // TODO: should use the |selectionArgs| argument in |ContentResolver.query()|.
+          ArrayList<String> restrictions = new ArrayList<String>();
+
+          if (mStartDate != 0) {
+            restrictions.add("date >= " + mStartDate);
+          }
+
+          if (mEndDate != 0) {
+            restrictions.add("date <= " + mEndDate);
+          }
+
+          if (mNumbersCount > 0) {
+            String numberRestriction = "address IN ('" + mNumbers[0] + "'";
+
+            for (int i=1; i<mNumbersCount; ++i) {
+              numberRestriction += ", '" + mNumbers[i] + "'";
+            }
+            numberRestriction += ")";
+
+            restrictions.add(numberRestriction);
+          }
+
+          if (mDeliveryState == kDeliveryStateUnknown) {
+            restrictions.add("type IN ('" + kSmsTypeSentbox + "', '" + kSmsTypeInbox + "')");
+          } else if (mDeliveryState == kDeliveryStateSent) {
+            restrictions.add("type = " + kSmsTypeSentbox);
+          } else if (mDeliveryState == kDeliveryStateReceived) {
+            restrictions.add("type = " + kSmsTypeInbox);
+          } else {
+            throw new UnexpectedDeliveryStateException();
+          }
+
+          String restrictionText = restrictions.size() > 0 ? restrictions.get(0) : "";
+
+          for (int i=1; i<restrictions.size(); ++i) {
+            restrictionText += " AND " + restrictions.get(i);
+          }
+
+          ContentResolver cr = GeckoApp.mAppContext.getContentResolver();
+          cursor = cr.query(kSmsContentUri, kRequiredMessageRows, restrictionText, null,
+                            mReverse ? "date DESC" : "date ASC");
+
+          if (cursor.getCount() == 0) {
+            GeckoAppShell.notifyNoMessageInList(mRequestId, mProcessId);
+            return;
+          }
+
+          cursor.moveToFirst();
+
+          int type = cursor.getInt(cursor.getColumnIndex("type"));
+          String sender = "";
+          String receiver = "";
+
+          if (type == kSmsTypeInbox) {
+            sender = cursor.getString(cursor.getColumnIndex("address"));
+          } else if (type == kSmsTypeSentbox) {
+            receiver = cursor.getString(cursor.getColumnIndex("address"));
+          } else {
+            throw new UnexpectedDeliveryStateException();
+          }
+
+          int listId = MessagesListManager.getInstance().add(cursor);
+          closeCursor = false;
+          GeckoAppShell.notifyListCreated(listId,
+                                          cursor.getInt(cursor.getColumnIndex("_id")),
+                                          receiver, sender,
+                                          cursor.getString(cursor.getColumnIndex("body")),
+                                          cursor.getLong(cursor.getColumnIndex("date")),
+                                          mRequestId, mProcessId);
+        } catch (UnexpectedDeliveryStateException e) {
+          Log.e("GeckoSmsManager", "Unexcepted delivery state type: " + e);
+          GeckoAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId, mProcessId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to create a message list cursor: " + e);
+          GeckoAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId, mProcessId);
+        } finally {
+          // Close the cursor if MessagesListManager isn't taking care of it.
+          // We could also just check if it is in the MessagesListManager list but
+          // that would be less efficient.
+          if (cursor != null && closeCursor) {
+            cursor.close();
+          }
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new CreateMessageListRunnable(aStartDate, aEndDate, aNumbers, aNumbersCount, aDeliveryState, aReverse, aRequestId, aProcessId))) {
+      Log.e("GeckoSmsManager", "Failed to add CreateMessageListRunnable to the SmsIOThread");
+      GeckoAppShell.notifyReadingMessageListFailed(kUnknownError, aRequestId, aProcessId);
+    }
+  }
+
+  public static void getNextMessageInList(int aListId, int aRequestId, long aProcessId) {
+    class GetNextMessageInListRunnable implements Runnable {
+      private int mListId;
+      private int mRequestId;
+      private long mProcessId;
+
+      GetNextMessageInListRunnable(int aListId, int aRequestId, long aProcessId) {
+        mListId = aListId;
+        mRequestId = aRequestId;
+        mProcessId = aProcessId;
+      }
+
+      @Override
+      public void run() {
+        class UnexpectedDeliveryStateException extends Exception { };
+
+        try {
+          Cursor cursor = MessagesListManager.getInstance().get(mListId);
+
+          if (!cursor.moveToNext()) {
+            MessagesListManager.getInstance().remove(mListId);
+            GeckoAppShell.notifyNoMessageInList(mRequestId, mProcessId);
+            return;
+          }
+
+          int type = cursor.getInt(cursor.getColumnIndex("type"));
+          String sender = "";
+          String receiver = "";
+
+          if (type == kSmsTypeInbox) {
+            sender = cursor.getString(cursor.getColumnIndex("address"));
+          } else if (type == kSmsTypeSentbox) {
+            receiver = cursor.getString(cursor.getColumnIndex("address"));
+          } else {
+            throw new UnexpectedDeliveryStateException();
+          }
+
+          int listId = MessagesListManager.getInstance().add(cursor);
+          GeckoAppShell.notifyGotNextMessage(cursor.getInt(cursor.getColumnIndex("_id")),
+                                             receiver, sender,
+                                             cursor.getString(cursor.getColumnIndex("body")),
+                                             cursor.getLong(cursor.getColumnIndex("date")),
+                                             mRequestId, mProcessId);
+        } catch (UnexpectedDeliveryStateException e) {
+          Log.e("GeckoSmsManager", "Unexcepted delivery state type: " + e);
+          GeckoAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId, mProcessId);
+        } catch (Exception e) {
+          Log.e("GeckoSmsManager", "Error while trying to get the next message of a list: " + e);
+          GeckoAppShell.notifyReadingMessageListFailed(kUnknownError, mRequestId, mProcessId);
+        }
+      }
+    }
+
+    if (!SmsIOThread.getInstance().execute(new GetNextMessageInListRunnable(aListId, aRequestId, aProcessId))) {
+      Log.e("GeckoSmsManager", "Failed to add GetNextMessageInListRunnable to the SmsIOThread");
+      GeckoAppShell.notifyReadingMessageListFailed(kUnknownError, aRequestId, aProcessId);
+    }
+  }
+
+  public static void clearMessageList(int aListId) {
+    MessagesListManager.getInstance().remove(aListId);
+  }
+
+  public static void shutdown() {
+    SmsIOThread.getInstance().interrupt();
+    MessagesListManager.getInstance().clear();
+  }
 }
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -159,16 +159,17 @@
 @BINPATH@/components/dom_geolocation.xpt
 @BINPATH@/components/dom_notification.xpt
 @BINPATH@/components/dom_html.xpt
 @BINPATH@/components/dom_indexeddb.xpt
 @BINPATH@/components/dom_offline.xpt
 @BINPATH@/components/dom_json.xpt
 @BINPATH@/components/dom_range.xpt
 @BINPATH@/components/dom_sidebar.xpt
+@BINPATH@/components/dom_sms.xpt
 @BINPATH@/components/dom_storage.xpt
 @BINPATH@/components/dom_stylesheets.xpt
 @BINPATH@/components/dom_threads.xpt
 @BINPATH@/components/dom_traversal.xpt
 @BINPATH@/components/dom_views.xpt
 @BINPATH@/components/dom_xbl.xpt
 @BINPATH@/components/dom_xpath.xpt
 @BINPATH@/components/dom_xul.xpt