Make GSM 7-bit encoding properly deal with initial padding.

For CDMA, clean up the GSM encapsulation to properly align
user data payload after the user data header.

Addresses http://buganizer/issue?id=2007011
diff --git a/telephony/java/com/android/internal/telephony/GsmAlphabet.java b/telephony/java/com/android/internal/telephony/GsmAlphabet.java
index e8095e1..461b694 100644
--- a/telephony/java/com/android/internal/telephony/GsmAlphabet.java
+++ b/telephony/java/com/android/internal/telephony/GsmAlphabet.java
@@ -183,15 +183,9 @@
         }
 
         int headerBits = (header.length + 1) * 8;
-        int headerSeptets = headerBits / 7;
-        headerSeptets += (headerBits % 7) > 0 ? 1 : 0;
+        int headerSeptets = (headerBits + 6) / 7;
 
-        int sz = data.length();
-        int septetCount;
-        septetCount = countGsmSeptets(data, true) + headerSeptets;
-
-        byte[] ret = stringToGsm7BitPacked(data, 0, septetCount,
-                (headerSeptets*7), true);
+        byte[] ret = stringToGsm7BitPacked(data, headerSeptets, true);
 
         // Paste in the header
         ret[1] = (byte)header.length;
@@ -215,7 +209,7 @@
      */
     public static byte[] stringToGsm7BitPacked(String data)
             throws EncodeException {
-        return stringToGsm7BitPacked(data, 0, -1, 0, true);
+        return stringToGsm7BitPacked(data, 0, true);
     }
 
     /**
@@ -228,58 +222,37 @@
      * septets.
      *
      * @param data the text to convert to septets
-     * @param dataOffset the character offset in data to start the encoding from
-     * @param maxSeptets the maximum number of septets to convert, or -1 for no
-     *  enforced maximum.
-     * @param startingBitOffset the number of padding bits to put before
-     *  the start of the first septet at the begining of the array
+     * @param startingSeptetOffset the number of padding septets to put before
+     *  the character data at the begining of the array
      * @param throwException If true, throws EncodeException on invalid char.
      *   If false, replaces unencodable char with GSM alphabet space char.
      *
      * @throws EncodeException if String is too large to encode
      */
-    public static byte[] stringToGsm7BitPacked(String data, int dataOffset,
-            int maxSeptets, int startingBitOffset, boolean throwException)
-            throws EncodeException {
-
-        int sz = data.length();
-        int septetCount;
-        if (maxSeptets == -1) {
-            septetCount = countGsmSeptets(data, true);
-        } else {
-            septetCount = maxSeptets;
+    public static byte[] stringToGsm7BitPacked(String data, int startingSeptetOffset,
+            boolean throwException) throws EncodeException {
+        int dataLen = data.length();
+        int septetCount = countGsmSeptets(data, throwException) + startingSeptetOffset;
+        if (septetCount > 255) {
+            throw new EncodeException("Payload cannot exceed 255 septets");
         }
-
-        if(septetCount > 0xff) {
-            throw new EncodeException("Payload cannot exceed " + Short.MAX_VALUE
-                    + " septets");
-        }
-
-        // Enough for all the septets and the length 2 byte prefix
-        byte[] ret = new byte[1 + (((septetCount * 7) + 7) / 8)];
-
-        int bitOffset = startingBitOffset;
-        int septets = startingBitOffset/7;
-        for (int i = dataOffset; i < sz && septets < septetCount; i++, bitOffset += 7) {
+        int byteCount = ((septetCount * 7) + 7) / 8;
+        byte[] ret = new byte[byteCount + 1];  // Include space for one byte length prefix.
+        for (int i = 0, septets = startingSeptetOffset, bitOffset = startingSeptetOffset * 7;
+                 i < dataLen && septets < septetCount;
+                 i++, bitOffset += 7) {
             char c = data.charAt(i);
-
             int v = GsmAlphabet.charToGsm(c, throwException);
             if (v == GSM_EXTENDED_ESCAPE) {
-                // Lookup the extended char
-                v = GsmAlphabet.charToGsmExtended(c);
-
+                v = GsmAlphabet.charToGsmExtended(c);  // Lookup the extended char.
                 packSmsChar(ret, bitOffset, GSM_EXTENDED_ESCAPE);
                 bitOffset += 7;
                 septets++;
             }
-
             packSmsChar(ret, bitOffset, v);
             septets++;
         }
-
-        // See check for > 0xff above
-        ret[0] = (byte)septets;
-
+        ret[0] = (byte) (septetCount);  // Validated by check above.
         return ret;
     }
 
diff --git a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java
index 7f3b473..65754fc 100644
--- a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java
+++ b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java
@@ -455,53 +455,114 @@
         }
     }
 
-    private static int calcUdhSeptetPadding(int userDataHeaderLen) {
-        int udhBits = userDataHeaderLen * 8;
-        int udhSeptets = (udhBits + 6) / 7;
-        int paddingBits = (udhSeptets * 7) - udhBits;
-        return paddingBits;
+    private static class Gsm7bitCodingResult {
+        int septets;
+        byte[] data;
     }
 
-    private static byte[] encode7bitGsm(String msg, int paddingBits)
+    private static Gsm7bitCodingResult encode7bitGsm(String msg, int septetOffset, boolean force)
         throws CodingException
     {
         try {
             /*
              * TODO(cleanup): It would be nice if GsmAlphabet provided
              * an option to produce just the data without prepending
-             * the length.
+             * the septet count, as this function is really just a
+             * wrapper to strip that off.  Not to mention that the
+             * septet count is generally known prior to invocation of
+             * the encoder.  Note that it cannot be derived from the
+             * resulting array length, since that cannot distinguish
+             * if the last contains either 1 or 8 valid bits.
+             *
+             * TODO(cleanup): The BitwiseXStreams could also be
+             * extended with byte-wise reversed endianness read/write
+             * routines to allow a corresponding implementation of
+             * stringToGsm7BitPacked, and potentially directly support
+             * access to the main bitwise stream from encode/decode.
              */
-            byte []fullData = GsmAlphabet.stringToGsm7BitPacked(msg, 0, -1, paddingBits, true);
-            byte []data = new byte[fullData.length - 1];
-            System.arraycopy(fullData, 1, data, 0, fullData.length - 1);
-            return data;
+            byte[] fullData = GsmAlphabet.stringToGsm7BitPacked(msg, septetOffset, !force);
+            Gsm7bitCodingResult result = new Gsm7bitCodingResult();
+            result.data = new byte[fullData.length - 1];
+            System.arraycopy(fullData, 1, result.data, 0, fullData.length - 1);
+            result.septets = fullData[0];
+            return result;
         } catch (com.android.internal.telephony.EncodeException ex) {
             throw new CodingException("7bit GSM encode failed: " + ex);
         }
     }
 
+    private static void encode7bitEms(UserData uData, byte[] udhData, boolean force)
+        throws CodingException
+    {
+        int udhBytes = udhData.length + 1;  // Add length octet.
+        int udhSeptets = ((udhBytes * 8) + 6) / 7;
+        Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, udhSeptets, force);
+        uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
+        uData.numFields = gcr.septets;
+        uData.payload = gcr.data;
+        uData.payload[0] = (byte)udhData.length;
+        System.arraycopy(udhData, 0, uData.payload, 1, udhData.length);
+    }
+
+    private static void encode16bitEms(UserData uData, byte[] udhData)
+        throws CodingException
+    {
+        byte[] payload = encodeUtf16(uData.payloadStr);
+        int udhBytes = udhData.length + 1;  // Add length octet.
+        int udhCodeUnits = (udhBytes + 1) / 2;
+        int udhPadding = udhBytes % 2;
+        int payloadCodeUnits = payload.length / 2;
+        uData.numFields = udhCodeUnits + payloadCodeUnits;
+        uData.payload = new byte[uData.numFields * 2];
+        uData.payload[0] = (byte)udhData.length;
+        System.arraycopy(udhData, 0, uData.payload, 1, udhData.length);
+        System.arraycopy(payload, 0, uData.payload, udhBytes + udhPadding, payload.length);
+    }
+
+    private static void encodeEmsUserDataPayload(UserData uData)
+        throws CodingException
+    {
+        byte[] headerData = SmsHeader.toByteArray(uData.userDataHeader);
+        if (uData.msgEncodingSet) {
+            if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) {
+                encode7bitEms(uData, headerData, true);
+            } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) {
+                encode16bitEms(uData, headerData);
+            } else {
+                throw new CodingException("unsupported EMS user data encoding (" +
+                                          uData.msgEncoding + ")");
+            }
+        } else {
+            try {
+                encode7bitEms(uData, headerData, false);
+            } catch (CodingException ex) {
+                encode16bitEms(uData, headerData);
+            }
+        }
+    }
+
     private static void encodeUserDataPayload(UserData uData)
         throws CodingException
     {
-        // TODO(cleanup): UDH can only occur in EMS mode, meaning
-        // encapsulation of GSM encoding, and so the logic here should
-        // be refactored to more cleanly reflect this constraint.
+        if ((uData.payloadStr == null) && (uData.msgEncoding != UserData.ENCODING_OCTET)) {
+            Log.e(LOG_TAG, "user data with null payloadStr");
+            uData.payloadStr = "";
+        }
 
-        byte[] headerData = null;
-        if (uData.userDataHeader != null) headerData = SmsHeader.toByteArray(uData.userDataHeader);
-        int headerDataLen = (headerData == null) ? 0 : headerData.length + 1;  // + length octet
+        if (uData.userDataHeader != null) {
+            encodeEmsUserDataPayload(uData);
+            return;
+        }
 
-        byte[] payloadData;
-        int codeUnitCount;
         if (uData.msgEncodingSet) {
             if (uData.msgEncoding == UserData.ENCODING_OCTET) {
                 if (uData.payload == null) {
                     Log.e(LOG_TAG, "user data with octet encoding but null payload");
-                    payloadData = new byte[0];
-                    codeUnitCount = 0;
+                    uData.payload = new byte[0];
+                    uData.numFields = 0;
                 } else {
-                    payloadData = uData.payload;
-                    codeUnitCount = uData.payload.length;
+                    uData.payload = uData.payload;
+                    uData.numFields = uData.payload.length;
                 }
             } else {
                 if (uData.payloadStr == null) {
@@ -509,65 +570,48 @@
                     uData.payloadStr = "";
                 }
                 if (uData.msgEncoding == UserData.ENCODING_GSM_7BIT_ALPHABET) {
-                    int paddingBits = calcUdhSeptetPadding(headerDataLen);
-                    payloadData = encode7bitGsm(uData.payloadStr, paddingBits);
-                    codeUnitCount = ((payloadData.length + headerDataLen) * 8) / 7;
+                    Gsm7bitCodingResult gcr = encode7bitGsm(uData.payloadStr, 0, true);
+                    uData.payload = gcr.data;
+                    uData.numFields = gcr.septets;
                 } else if (uData.msgEncoding == UserData.ENCODING_7BIT_ASCII) {
-                    payloadData = encode7bitAscii(uData.payloadStr, true);
-                    codeUnitCount = uData.payloadStr.length();
+                    uData.payload = encode7bitAscii(uData.payloadStr, true);
+                    uData.numFields = uData.payloadStr.length();
                 } else if (uData.msgEncoding == UserData.ENCODING_UNICODE_16) {
-                    payloadData = encodeUtf16(uData.payloadStr);
-                    codeUnitCount = uData.payloadStr.length();
+                    uData.payload = encodeUtf16(uData.payloadStr);
+                    uData.numFields = uData.payloadStr.length();
                 } else {
                     throw new CodingException("unsupported user data encoding (" +
                                               uData.msgEncoding + ")");
                 }
             }
         } else {
-            if (uData.payloadStr == null) {
-                Log.e(LOG_TAG, "user data with null payloadStr");
-                uData.payloadStr = "";
-            }
             try {
-                if (headerData == null) {
-                    payloadData = encode7bitAscii(uData.payloadStr, false);
-                    codeUnitCount = uData.payloadStr.length();
-                    uData.msgEncoding = UserData.ENCODING_7BIT_ASCII;
-                } else {
-                    // If there is a header, we are in EMS mode, in
-                    // which case we use GSM encodings.
-                    int paddingBits = calcUdhSeptetPadding(headerDataLen);
-                    payloadData = encode7bitGsm(uData.payloadStr, paddingBits);
-                    codeUnitCount = ((payloadData.length + headerDataLen) * 8) / 7;
-                    uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
-                }
+                uData.payload = encode7bitAscii(uData.payloadStr, false);
+                uData.msgEncoding = UserData.ENCODING_7BIT_ASCII;
             } catch (CodingException ex) {
-                payloadData = encodeUtf16(uData.payloadStr);
-                codeUnitCount = uData.payloadStr.length();
+                uData.payload = encodeUtf16(uData.payloadStr);
                 uData.msgEncoding = UserData.ENCODING_UNICODE_16;
             }
+            uData.numFields = uData.payloadStr.length();
             uData.msgEncodingSet = true;
         }
-
-        int totalLength = payloadData.length + headerDataLen;
-        if (totalLength > SmsMessage.MAX_USER_DATA_BYTES) {
-            throw new CodingException("encoded user data too large (" + totalLength +
-                                      " > " + SmsMessage.MAX_USER_DATA_BYTES + " bytes)");
-        }
-
-        uData.numFields = codeUnitCount;
-        uData.payload = new byte[totalLength];
-        if (headerData != null) {
-            uData.payload[0] = (byte)headerData.length;
-            System.arraycopy(headerData, 0, uData.payload, 1, headerData.length);
-        }
-        System.arraycopy(payloadData, 0, uData.payload, headerDataLen, payloadData.length);
     }
 
     private static void encodeUserData(BearerData bData, BitwiseOutputStream outStream)
         throws BitwiseOutputStream.AccessException, CodingException
     {
+        /*
+         * TODO(cleanup): Do we really need to set userData.payload as
+         * a side effect of encoding?  If not, we could avoid data
+         * copies by passing outStream directly.
+         */
         encodeUserDataPayload(bData.userData);
+        if (bData.userData.payload.length > SmsMessage.MAX_USER_DATA_BYTES) {
+            throw new CodingException("encoded user data too large (" +
+                                      bData.userData.payload.length +
+                                      " > " + SmsMessage.MAX_USER_DATA_BYTES + " bytes)");
+        }
+
         /**
          * XXX/TODO: figure out what the right answer is WRT padding bits
          *
@@ -846,6 +890,9 @@
     private static String decodeUtf16(byte[] data, int offset, int numFields)
         throws CodingException
     {
+        // Start reading from the next 16-bit aligned boundry after offset.
+        int padding = offset % 2;
+        numFields -= (offset + padding) / 2;
         try {
             return new String(data, offset, numFields * 2, "utf-16be");
         } catch (java.io.UnsupportedEncodingException ex) {
@@ -889,11 +936,11 @@
     private static String decode7bitGsm(byte[] data, int offset, int numFields)
         throws CodingException
     {
-        int paddingBits = calcUdhSeptetPadding(offset);
-        numFields -= (((offset * 8) + paddingBits) / 7);
-        // TODO: It seems wrong that only Gsm7 bit encodings would
-        // take into account the header in numFields calculations.
-        // This should be verified.
+        // Start reading from the next 7-bit aligned boundry after offset.
+        int offsetBits = offset * 8;
+        int offsetSeptets = (offsetBits + 6) / 7;
+        numFields -= offsetSeptets;
+        int paddingBits = (offsetSeptets * 7) - offsetBits;
         String result = GsmAlphabet.gsm7BitPackedToString(data, offset, numFields, paddingBits);
         if (result == null) {
             throw new CodingException("7bit GSM decoding failed");
diff --git a/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java b/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java
index 365fee8..02af547 100644
--- a/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java
+++ b/tests/AndroidTests/src/com/android/unit_tests/CdmaSmsTest.java
@@ -18,9 +18,11 @@
 
 import com.android.internal.telephony.GsmAlphabet;
 import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.cdma.SmsMessage;
 import com.android.internal.telephony.cdma.sms.BearerData;
 import com.android.internal.telephony.cdma.sms.UserData;
 import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
+import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails;
 import com.android.internal.util.BitwiseInputStream;
 import com.android.internal.util.BitwiseOutputStream;
 import com.android.internal.util.HexDump;
@@ -28,12 +30,12 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import android.util.Log;
+
 import java.util.Iterator;
 
 import java.lang.Integer;
 
-import android.util.Log;
-
 public class CdmaSmsTest extends AndroidTestCase {
     private final static String LOG_TAG = "CDMA";
 
@@ -151,6 +153,9 @@
         userData.payloadStr = "Test \n standard \r SMS";
         revBearerData = BearerData.decode(BearerData.encode(bearerData));
         assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        userData.payloadStr = "";
+        revBearerData = BearerData.decode(BearerData.encode(bearerData));
+        assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
     }
 
     @SmallTest
@@ -172,6 +177,21 @@
         assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding);
         assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields);
         assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        userData.payloadStr = "1234567";
+        revBearerData = BearerData.decode(BearerData.encode(bearerData));
+        assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        userData.payloadStr = "";
+        revBearerData = BearerData.decode(BearerData.encode(bearerData));
+        assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        userData.payloadStr = "12345678901234567890123456789012345678901234567890" +
+                "12345678901234567890123456789012345678901234567890" +
+                "12345678901234567890123456789012345678901234567890" +
+                "1234567890";
+        revBearerData = BearerData.decode(BearerData.encode(bearerData));
+        assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        userData.payloadStr = "Test \u007f illegal \u0000 SMS chars";
+        revBearerData = BearerData.decode(BearerData.encode(bearerData));
+        assertEquals("Test   illegal   SMS chars", revBearerData.userData.payloadStr);
         userData.payloadStr = "More @ testing\nis great^|^~woohoo";
         revBearerData = BearerData.decode(BearerData.encode(bearerData));
         assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
@@ -220,6 +240,12 @@
         assertEquals(userData.msgEncoding, revBearerData.userData.msgEncoding);
         assertEquals(userData.payloadStr.length(), revBearerData.userData.numFields);
         assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        userData.payloadStr = "1234567";
+        revBearerData = BearerData.decode(BearerData.encode(bearerData));
+        assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+        userData.payloadStr = "";
+        revBearerData = BearerData.decode(BearerData.encode(bearerData));
+        assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
     }
 
     @SmallTest
@@ -784,4 +810,24 @@
         assertEquals(bd4.userData.payloadStr, "ABCDEFG");
     }
 
+    @SmallTest
+    public void testUserDataHeaderWithEightCharMsg() throws Exception {
+        BearerData bearerData = new BearerData();
+        bearerData.messageType = BearerData.MESSAGE_TYPE_DELIVER;
+        bearerData.messageId = 55;
+        SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
+        concatRef.refNumber = 0xEE;
+        concatRef.msgCount = 2;
+        concatRef.seqNumber = 2;
+        concatRef.isEightBits = true;
+        SmsHeader smsHeader = new SmsHeader();
+        smsHeader.concatRef = concatRef;
+        UserData userData = new UserData();
+        userData.payloadStr = "01234567";
+        userData.userDataHeader = smsHeader;
+        bearerData.userData = userData;
+        byte[] encodedSms = BearerData.encode(bearerData);
+        BearerData revBearerData = BearerData.decode(encodedSms);
+        assertEquals(userData.payloadStr, revBearerData.userData.payloadStr);
+    }
 }