Bug 712933 - Part 3: fix getNumberOfMessagesForText(). r=philikon
authorVicamo Yang <vyang@mozilla.com>
Fri, 16 Mar 2012 16:57:41 -0700
changeset 89584 4da9c8dd18f0929ddb7dc5fda03badc85c4b3410
parent 89583 7a535f81b19baeb87746976b4dea9296606c4a4b
child 89585 66512e033f3e0279a7d506467852876a784f1d1c
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersphilikon
bugs712933
milestone14.0a1
Bug 712933 - Part 3: fix getNumberOfMessagesForText(). r=philikon
dom/system/gonk/RadioInterfaceLayer.js
dom/system/gonk/ril_consts.js
dom/system/gonk/tests/test_ril_worker_sms.js
--- a/dom/system/gonk/RadioInterfaceLayer.js
+++ b/dom/system/gonk/RadioInterfaceLayer.js
@@ -529,22 +529,31 @@ RadioInterfaceLayer.prototype = {
     gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_IN_CALL; // XXX why is this needed?
     let force = value ? nsIAudioManager.FORCE_SPEAKER :
                         nsIAudioManager.FORCE_NONE;
     gAudioManager.setForceForUse(nsIAudioManager.USE_COMMUNICATION, force);
   },
 
   /**
    * List of tuples of national language identifier pairs.
+   *
+   * TODO: Support static/runtime settings, see bug 733331.
    */
   enabledGsmTableTuples: [
     [RIL.PDU_NL_IDENTIFIER_DEFAULT, RIL.PDU_NL_IDENTIFIER_DEFAULT],
   ],
 
   /**
+   * Use 16-bit reference number for concatenated outgoint messages.
+   *
+   * TODO: Support static/runtime settings, see bug 733331.
+   */
+  segmentRef16Bit: false,
+
+  /**
    * Calculate encoded length using specified locking/single shift table
    *
    * @param message
    *        message string to be encoded.
    * @param langTable
    *        locking shift table string.
    * @param langShiftTable
    *        single shift table string.
@@ -597,22 +606,21 @@ RadioInterfaceLayer.prototype = {
    * Calculate user data length of specified message string encoded in GSM 7Bit
    * alphabets.
    *
    * @param message
    *        a message string to be encoded.
    *
    * @return null or an options object with attributes `dcs`,
    *         `userDataHeaderLength`, `encodedBodyLength`, `langIndex`,
-   *         `langShiftIndex` set.
+   *         `langShiftIndex`, `segmentMaxSeq` set.
    *
    * @see #_calculateUserDataLength().
    */
   _calculateUserDataLength7Bit: function _calculateUserDataLength7Bit(message) {
-    //TODO: support multipart SMS, see bug 712933
     let options = null;
     let minUserDataSeptets = Number.MAX_VALUE;
     for (let i = 0; i < this.enabledGsmTableTuples.length; i++) {
       let [langIndex, langShiftIndex] = this.enabledGsmTableTuples[i];
 
       const langTable = RIL.PDU_NL_LOCKING_SHIFT_TABLES[langIndex];
       const langShiftTable = RIL.PDU_NL_SINGLE_SHIFT_TABLES[langShiftIndex];
 
@@ -629,50 +637,82 @@ RadioInterfaceLayer.prototype = {
       }
       if (langShiftIndex != RIL.PDU_NL_IDENTIFIER_DEFAULT) {
         headerLen += 3; // IEI + len + langShiftIndex
       }
 
       // Calculate full user data length, note the extra byte is for header len
       let headerSeptets = Math.ceil((headerLen ? headerLen + 1 : 0) * 8 / 7);
       let userDataSeptets = bodySeptets + headerSeptets;
+      let segments = bodySeptets ? 1 : 0;
+      if (userDataSeptets > RIL.PDU_MAX_USER_DATA_7BIT) {
+        if (this.segmentRef16Bit) {
+          headerLen += 6;
+        } else {
+          headerLen += 5;
+        }
+
+        headerSeptets = Math.ceil((headerLen + 1) * 8 / 7);
+        let segmentSeptets = RIL.PDU_MAX_USER_DATA_7BIT - headerSeptets;
+        segments = Math.ceil(bodySeptets / segmentSeptets);
+        userDataSeptets = bodySeptets + headerSeptets * segments;
+      }
+
       if (userDataSeptets >= minUserDataSeptets) {
         continue;
       }
 
       minUserDataSeptets = userDataSeptets;
 
       options = {
         dcs: RIL.PDU_DCS_MSG_CODING_7BITS_ALPHABET,
         encodedBodyLength: bodySeptets,
         userDataHeaderLength: headerLen,
         langIndex: langIndex,
         langShiftIndex: langShiftIndex,
+        segmentMaxSeq: segments,
       };
     }
 
     return options;
   },
 
   /**
    * Calculate user data length of specified message string encoded in UCS2.
    *
    * @param message
    *        a message string to be encoded.
    *
    * @return an options object with attributes `dcs`, `userDataHeaderLength`,
-   *         `encodedBodyLength` set.
+   *         `encodedBodyLength`, `segmentMaxSeq` set.
    *
    * @see #_calculateUserDataLength().
    */
   _calculateUserDataLengthUCS2: function _calculateUserDataLengthUCS2(message) {
+    let bodyChars = message.length;
+    let headerLen = 0;
+    let headerChars = Math.ceil((headerLen ? headerLen + 1 : 0) / 2);
+    let segments = bodyChars ? 1 : 0;
+    if ((bodyChars + headerChars) > RIL.PDU_MAX_USER_DATA_UCS2) {
+      if (this.segmentRef16Bit) {
+        headerLen += 6;
+      } else {
+        headerLen += 5;
+      }
+
+      headerChars = Math.ceil((headerLen + 1) / 2);
+      let segmentChars = RIL.PDU_MAX_USER_DATA_UCS2 - headerChars;
+      segments = Math.ceil(bodyChars / segmentChars);
+    }
+
     return {
       dcs: RIL.PDU_DCS_MSG_CODING_16BITS_ALPHABET,
-      encodedBodyLength: message.length * 2,
-      userDataHeaderLength: 0,
+      encodedBodyLength: bodyChars * 2,
+      userDataHeaderLength: headerLen,
+      segmentMaxSeq: segments,
     };
   },
 
   /**
    * Calculate user data length and its encoding.
    *
    * @param message
    *        a message string to be encoded.
@@ -689,36 +729,166 @@ RadioInterfaceLayer.prototype = {
    *        size will be userDataHeaderLength + 1; 0 for no header.
    * @param encodedBodyLength
    *        Length of the message body when encoded with the given DCS. For
    *        UCS2, in bytes; for 7-bit, in septets.
    * @param langIndex
    *        Table index used for normal 7-bit encoded character lookup.
    * @param langShiftIndex
    *        Table index used for escaped 7-bit encoded character lookup.
+   * @param segmentMaxSeq
+   *        Max sequence number of a multi-part messages, or 1 for single one.
+   *        This number might not be accurate for a multi-part message until
+   *        it's processed by #_fragmentText() again.
    */
   _calculateUserDataLength: function _calculateUserDataLength(message) {
     let options = this._calculateUserDataLength7Bit(message);
     if (!options) {
       options = this._calculateUserDataLengthUCS2(message);
     }
 
     if (options) {
       options.body = message;
     }
 
     debug("_calculateUserDataLength: " + JSON.stringify(options));
     return options;
   },
 
+  /**
+   * Fragment GSM 7-Bit encodable string for transmission.
+   *
+   * @param text
+   *        text string to be fragmented.
+   * @param langTable
+   *        locking shift table string.
+   * @param langShiftTable
+   *        single shift table string.
+   * @param headerLen
+   *        Length of prepended user data header.
+   *
+   * @return an array of objects. See #_fragmentText() for detailed definition.
+   */
+  _fragmentText7Bit: function _fragmentText7Bit(text, langTable, langShiftTable, headerLen) {
+    const headerSeptets = Math.ceil((headerLen ? headerLen + 1 : 0) * 8 / 7);
+    const segmentSeptets = RIL.PDU_MAX_USER_DATA_7BIT - headerSeptets;
+    let ret = [];
+    let begin = 0, len = 0;
+    for (let i = 0, inc = 0; i < text.length; i++) {
+      let septet = langTable.indexOf(text.charAt(i));
+      if (septet == RIL.PDU_NL_EXTENDED_ESCAPE) {
+        continue;
+      }
+
+      if (septet >= 0) {
+        inc = 1;
+      } else {
+        septet = langShiftTable.indexOf(text.charAt(i));
+        if (septet < 0) {
+          throw new Error("Given text cannot be encoded with GSM 7-bit Alphabet!");
+        }
+
+        if (septet == RIL.PDU_NL_RESERVED_CONTROL) {
+          continue;
+        }
+
+        inc = 2;
+      }
+
+      if ((len + inc) > segmentSeptets) {
+        ret.push({
+          body: text.substring(begin, i),
+          encodedBodyLength: len,
+        });
+        begin = i;
+        len = 0;
+      }
+
+      len += inc;
+    }
+
+    if (len) {
+      ret.push({
+        body: text.substring(begin),
+        encodedBodyLength: len,
+      });
+    }
+
+    return ret;
+  },
+
+  /**
+   * Fragment UCS2 encodable string for transmission.
+   *
+   * @param text
+   *        text string to be fragmented.
+   * @param headerLen
+   *        Length of prepended user data header.
+   *
+   * @return an array of objects. See #_fragmentText() for detailed definition.
+   */
+  _fragmentTextUCS2: function _fragmentTextUCS2(text, headerLen) {
+    const headerChars = Math.ceil((headerLen ? headerLen + 1 : 0) / 2);
+    const segmentChars = RIL.PDU_MAX_USER_DATA_UCS2 - headerChars;
+    let ret = [];
+    for (let offset = 0; offset < text.length; offset += segmentChars) {
+      let str = text.substr(offset, segmentChars);
+      ret.push({
+        body: str,
+        encodedBodyLength: str.length * 2,
+      });
+    }
+
+    return ret;
+  },
+
+  /**
+   * Fragment string for transmission.
+   *
+   * Fragment input text string into an array of objects that contains
+   * attributes `body`, substring for this segment, `encodedBodyLength`,
+   * length of the encoded segment body in septets.
+   *
+   * @param text
+   *        Text string to be fragmented.
+   * @param options
+   *        Optional pre-calculated option object. The output array will be
+   *        stored at options.segments if there are multiple segments.
+   *
+   * @return Populated options object.
+   */
+  _fragmentText: function _fragmentText(text, options) {
+    if (!options) {
+      options = this._calculateUserDataLength(text);
+    }
+
+    if (options.segmentMaxSeq <= 1) {
+      options.segments = null;
+      return options;
+    }
+
+    if (options.dcs == RIL.PDU_DCS_MSG_CODING_7BITS_ALPHABET) {
+      const langTable = RIL.PDU_NL_LOCKING_SHIFT_TABLES[options.langIndex];
+      const langShiftTable = RIL.PDU_NL_SINGLE_SHIFT_TABLES[options.langShiftIndex];
+      options.segments = this._fragmentText7Bit(options.body,
+                                                langTable, langShiftTable,
+                                                options.userDataHeaderLength);
+    } else {
+      options.segments = this._fragmentTextUCS2(options.body,
+                                                options.userDataHeaderLength);
+    }
+
+    // Re-sync options.segmentMaxSeq with actual length of returning array.
+    options.segmentMaxSeq = options.segments.length;
+
+    return options;
+  },
+
   getNumberOfMessagesForText: function getNumberOfMessagesForText(text) {
-    //TODO: this assumes 7bit encoding, which is incorrect. Need to look
-    // for characters not supported by 7bit alphabets and then calculate
-    // length in UCS2 encoding.
-    return Math.ceil(text.length / 160);
+    return this._fragmentText(text).segmentMaxSeq;
   },
 
   sendSMS: function sendSMS(number, message, requestId, processId) {
     let options = this._calculateUserDataLength(message);
     options.type = "sendSMS";
     options.number = number;
     options.requestId = requestId;
     options.processId = processId;
--- a/dom/system/gonk/ril_consts.js
+++ b/dom/system/gonk/ril_consts.js
@@ -408,18 +408,22 @@ const PDU_VPF_ENHANCED = 0x8; // Validit
 const PDU_MMS_RD       = 0x04;// More messages to send. (SMS-DELIVER only) or
                               // Reject duplicates (SMS-SUBMIT only)
 
 // MTI - Message Type Indicator
 const PDU_MTI_SMS_STATUS_COMMAND  = 0x02;
 const PDU_MTI_SMS_SUBMIT          = 0x01;
 const PDU_MTI_SMS_DELIVER         = 0x00;
 
+// User Data max length in septets
+const PDU_MAX_USER_DATA_7BIT = 160;
 // User Data max length in octets
-const PDU_MAX_USER_DATA_7BIT = 160;
+const PDU_MAX_USER_DATA_8BIT = 140;
+// User Data max length in chars
+const PDU_MAX_USER_DATA_UCS2 = 70;
 
 // DCS - Data Coding Scheme
 const PDU_DCS_MSG_CODING_7BITS_ALPHABET = 0x00;
 const PDU_DCS_MSG_CODING_8BITS_ALPHABET = 0x04;
 const PDU_DCS_MSG_CODING_16BITS_ALPHABET= 0x08;
 const PDU_DCS_MSG_CLASS_ME_SPECIFIC     = 0xF1;
 const PDU_DCS_MSG_CLASS_SIM_SPECIFIC    = 0xF2;
 const PDU_DCS_MSG_CLASS_TE_SPECIFIC     = 0xF3;
--- a/dom/system/gonk/tests/test_ril_worker_sms.js
+++ b/dom/system/gonk/tests/test_ril_worker_sms.js
@@ -198,16 +198,111 @@ add_test(function test_RadioInterfaceLay
   //   get total cost 6 in the first run and returns immediately. With correct
   //   fix, the best choice should be the second one.
   test_calc(ESCAPE + ESCAPE + ESCAPE + ESCAPE + ESCAPE + "\\",
             [PDU_DCS_MSG_CODING_7BITS_ALPHABET, 2, 0, 0, 0], [[3, 0], [0, 0]]);
 
   run_next_test();
 });
 
+function generateStringOfLength(str, length) {
+  while (str.length < length) {
+    if (str.length < (length / 2)) {
+      str = str + str;
+    } else {
+      str = str + str.substr(0, length - str.length);
+    }
+  }
+
+  return str;
+}
+
+/**
+ * Verify RadioInterfaceLayer#_calculateUserDataLength7Bit multipart handling.
+ */
+add_test(function test_RadioInterfaceLayer__calculateUserDataLength7Bit_multipart() {
+  let ril = newRadioInterfaceLayer();
+
+  function test_calc(str, expected) {
+    let options = ril._calculateUserDataLength7Bit(str);
+
+    do_check_eq(expected[0], options.encodedBodyLength);
+    do_check_eq(expected[1], options.userDataHeaderLength);
+    do_check_eq(expected[2], options.segmentMaxSeq);
+  }
+
+  test_calc(generateStringOfLength("A", PDU_MAX_USER_DATA_7BIT),
+            [PDU_MAX_USER_DATA_7BIT, 0, 1]);
+  test_calc(generateStringOfLength("A", PDU_MAX_USER_DATA_7BIT + 1),
+            [PDU_MAX_USER_DATA_7BIT + 1, 5, 2]);
+
+  run_next_test();
+});
+
+/**
+ * Verify RadioInterfaceLayer#_fragmentText7Bit().
+ */
+add_test(function test_RadioInterfaceLayer__fragmentText7Bit() {
+  let ril = newRadioInterfaceLayer();
+
+  function test_calc(str, expected) {
+    let options = ril._fragmentText(str);
+    if (expected) {
+      do_check_eq(expected, options.segments.length);
+    } else {
+      do_check_eq(null, options.segments);
+    }
+  }
+
+  // Boundary checks
+  test_calc("");
+  test_calc(generateStringOfLength("A", PDU_MAX_USER_DATA_7BIT));
+  test_calc(generateStringOfLength("A", PDU_MAX_USER_DATA_7BIT + 1), 2);
+
+  // Escaped character
+  test_calc(generateStringOfLength("{", PDU_MAX_USER_DATA_7BIT / 2));
+  test_calc(generateStringOfLength("{", PDU_MAX_USER_DATA_7BIT / 2 + 1), 2);
+  // Escaped character cannot be separated
+  test_calc(generateStringOfLength("{", (PDU_MAX_USER_DATA_7BIT - 7) * 2 / 2), 3);
+
+  // Test headerLen, 7 = Math.ceil(6 * 8 / 7), 6 = headerLen + 1
+  test_calc(generateStringOfLength("A", PDU_MAX_USER_DATA_7BIT - 7));
+  test_calc(generateStringOfLength("A", (PDU_MAX_USER_DATA_7BIT - 7) * 2), 2);
+  test_calc(generateStringOfLength("A", (PDU_MAX_USER_DATA_7BIT - 7) * 3), 3);
+
+  run_next_test();
+});
+
+/**
+ * Verify RadioInterfaceLayer#_fragmentTextUCS2().
+ */
+add_test(function test_RadioInterfaceLayer__fragmentTextUCS2() {
+  let ril = newRadioInterfaceLayer();
+
+  function test_calc(str, expected) {
+    let options = ril._fragmentText(str);
+    if (expected) {
+      do_check_eq(expected, options.segments.length);
+    } else {
+      do_check_eq(null, options.segments);
+    }
+  }
+
+  // Boundary checks
+  test_calc(generateStringOfLength("\ua2db", PDU_MAX_USER_DATA_UCS2));
+  test_calc(generateStringOfLength("\ua2db", PDU_MAX_USER_DATA_UCS2 + 1), 2);
+
+  // UCS2 character cannot be separated
+  ril.segmentRef16Bit = true;
+  test_calc(generateStringOfLength("\ua2db", (PDU_MAX_USER_DATA_UCS2 * 2 - 7) * 2 / 2), 3);
+  ril.segmentRef16Bit = false;
+
+  run_next_test();
+});
+
 /**
  * Verify GsmPDUHelper#writeStringAsSeptets() padding bits handling.
  */
 add_test(function test_GsmPDUHelper_writeStringAsSeptets() {
   let worker = newWorker({
     postRILMessage: function fakePostRILMessage(data) {
       // Do nothing
     },