BluetoothMapSmsPdu.java revision 70be005a18a35ec5fcb46152f0dfbe82156efa3a
1/*
2* Copyright (C) 2013 Samsung System LSI
3* Licensed under the Apache License, Version 2.0 (the "License");
4* you may not use this file except in compliance with the License.
5* You may obtain a copy of the License at
6*
7*      http://www.apache.org/licenses/LICENSE-2.0
8*
9* Unless required by applicable law or agreed to in writing, software
10* distributed under the License is distributed on an "AS IS" BASIS,
11* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12* See the License for the specific language governing permissions and
13* limitations under the License.
14*/
15package com.android.bluetooth.map;
16
17import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA;
18import static com.android.internal.telephony.SmsConstants.ENCODING_7BIT;
19
20import java.io.ByteArrayInputStream;
21import java.io.ByteArrayOutputStream;
22import java.io.DataInputStream;
23import java.io.EOFException;
24import java.io.IOException;
25import java.io.UnsupportedEncodingException;
26import java.text.SimpleDateFormat;
27import java.util.ArrayList;
28import java.util.Calendar;
29import java.util.Date;
30import java.util.Random;
31
32import android.telephony.PhoneNumberUtils;
33import android.telephony.SmsMessage;
34import android.telephony.TelephonyManager;
35import android.util.Log;
36
37import com.android.internal.telephony.*;
38/*import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
39import com.android.internal.telephony.SmsConstants;*/
40import com.android.internal.telephony.SmsHeader;
41import com.android.internal.telephony.SmsMessageBase;
42import com.android.internal.telephony.SmsMessageBase.SubmitPduBase;
43import com.android.internal.telephony.cdma.sms.*;
44import com.android.internal.telephony.gsm.SmsMessage.SubmitPdu;
45
46public class BluetoothMapSmsPdu {
47
48    private static final String TAG = "BluetoothMapSmsPdu";
49    private static final boolean V = false;
50    private static int INVALID_VALUE = -1;
51    public static int SMS_TYPE_GSM = 1;
52    public static int SMS_TYPE_CDMA = 2;
53
54
55    /* TODO: We need to handle the SC-address mentioned in errata 4335.
56     * Since the definition could be read in three different ways, I have asked
57     * the car working group for clarification, and are awaiting confirmation that
58     * this clarification will go into the MAP spec:
59     *  "The native format should be <sc_addr><tpdu> where <sc_addr> is <length><ton><1..10 octet of address>
60     *   coded according to 24.011. The IEI is not to be used, as the fixed order of the data makes a type 4 LV
61     *   information element sufficient. <length> is a single octet which value is the length of the value-field
62     *   in octets including both the <ton> and the <address>."
63     * */
64
65
66    public static class SmsPdu {
67        private byte[] data;
68        private byte[] scAddress = {0}; // At the moment we do not use the scAddress, hence set the length to 0.
69        private int userDataMsgOffset = 0;
70        private int encoding;
71        private int languageTable;
72        private int languageShiftTable;
73        private int type;
74
75        /* Members used for pdu decoding */
76        private int userDataSeptetPadding = INVALID_VALUE;
77        private int msgSeptetCount = 0;
78
79        SmsPdu(byte[] data, int type){
80            this.data = data;
81            this.encoding = INVALID_VALUE;
82            this.type = type;
83            this.languageTable = INVALID_VALUE;
84            this.languageShiftTable = INVALID_VALUE;
85            this.userDataMsgOffset = gsmSubmitGetTpUdOffset(); // Assume no user data header
86        }
87
88        /**
89         * Create a pdu instance based on the data generated on this device.
90         * @param data
91         * @param encoding
92         * @param type
93         * @param languageTable
94         */
95        SmsPdu(byte[]data, int encoding, int type, int languageTable){
96            this.data = data;
97            this.encoding = encoding;
98            this.type = type;
99            this.languageTable = languageTable;
100        }
101        public byte[] getData(){
102            return data;
103        }
104        public byte[] getScAddress(){
105            return scAddress;
106        }
107        public void setEncoding(int encoding) {
108            this.encoding = encoding;
109        }
110        public int getEncoding(){
111            return encoding;
112        }
113        public int getType(){
114            return type;
115        }
116        public int getUserDataMsgOffset() {
117            return userDataMsgOffset;
118        }
119        /** The user data message payload size in bytes - excluding the user data header. */
120        public int getUserDataMsgSize() {
121            return data.length - userDataMsgOffset;
122        }
123
124        public int getLanguageShiftTable() {
125            return languageShiftTable;
126        }
127
128        public int getLanguageTable() {
129            return languageTable;
130        }
131
132        public int getUserDataSeptetPadding() {
133            return userDataSeptetPadding;
134        }
135
136        public int getMsgSeptetCount() {
137            return msgSeptetCount;
138        }
139
140
141        /* PDU parsing/modification functionality */
142        private final static byte TELESERVICE_IDENTIFIER                    = 0x00;
143        private final static byte SERVICE_CATEGORY                          = 0x01;
144        private final static byte ORIGINATING_ADDRESS                       = 0x02;
145        private final static byte ORIGINATING_SUB_ADDRESS                   = 0x03;
146        private final static byte DESTINATION_ADDRESS                       = 0x04;
147        private final static byte DESTINATION_SUB_ADDRESS                   = 0x05;
148        private final static byte BEARER_REPLY_OPTION                       = 0x06;
149        private final static byte CAUSE_CODES                               = 0x07;
150        private final static byte BEARER_DATA                               = 0x08;
151
152        /**
153         * Find and return the offset to the specified parameter ID
154         * @param parameterId The parameter ID to find
155         * @return the offset in number of bytes to the parameterID entry in the pdu data.
156         * The byte at the offset contains the parameter ID, the byte following contains the
157         * parameter length, and offset + 2 is the first byte of the parameter data.
158         */
159        private int cdmaGetParameterOffset(byte parameterId) {
160            ByteArrayInputStream pdu = new ByteArrayInputStream(data);
161            int offset = 0;
162            boolean found = false;
163
164            try {
165                pdu.skip(1); // Skip the message type
166
167                while (pdu.available() > 0) {
168                    int currentId = pdu.read();
169                    int currentLen = pdu.read();
170
171                    if(currentId == parameterId) {
172                        found = true;
173                        break;
174                    }
175                    else {
176                        pdu.skip(currentLen);
177                        offset += 2 + currentLen;
178                    }
179                }
180                pdu.close();
181            } catch (Exception e) {
182                Log.e(TAG, "cdmaGetParameterOffset: ", e);
183            }
184
185            if(found)
186                return offset;
187            else
188                return 0;
189        }
190
191        private final static byte BEARER_DATA_MSG_ID = 0x00;
192
193        private int cdmaGetSubParameterOffset(byte subParameterId) {
194            ByteArrayInputStream pdu = new ByteArrayInputStream(data);
195            int offset = 0;
196            boolean found = false;
197            offset = cdmaGetParameterOffset(BEARER_DATA) + 2; // Add to offset the BEARER_DATA parameter id and length bytes
198            pdu.skip(offset);
199            try {
200
201                while (pdu.available() > 0) {
202                    int currentId = pdu.read();
203                    int currentLen = pdu.read();
204
205                    if(currentId == subParameterId) {
206                        found = true;
207                        break;
208                    }
209                    else {
210                        pdu.skip(currentLen);
211                        offset += 2 + currentLen;
212                    }
213                }
214                pdu.close();
215            } catch (Exception e) {
216                Log.e(TAG, "cdmaGetParameterOffset: ", e);
217            }
218
219            if(found)
220                return offset;
221            else
222                return 0;
223        }
224
225
226        public void cdmaChangeToDeliverPdu(long date){
227            /* Things to change:
228             *  - Message Type in bearer data (Not the overall point-to-point type)
229             *  - Change address ID from destination to originating (sub addresses are not used)
230             *  - A time stamp is not mandatory.
231             */
232            int offset;
233            offset = cdmaGetParameterOffset(DESTINATION_ADDRESS);
234            data[offset] = ORIGINATING_ADDRESS;
235            offset = cdmaGetParameterOffset(DESTINATION_SUB_ADDRESS);
236            data[offset] = ORIGINATING_SUB_ADDRESS;
237
238            offset = cdmaGetSubParameterOffset(BEARER_DATA_MSG_ID);
239
240//            if(data != null && data.length > 2) {
241                int tmp = data[offset+2] & 0xff; // Skip the subParam ID and length, and read the first byte.
242                // Mask out the type
243                tmp &= 0x0f;
244                // Set the new type
245                tmp |= ((BearerData.MESSAGE_TYPE_DELIVER << 4) & 0xf0);
246                // Store the result
247                data[offset+2] = (byte) tmp;
248
249//            }
250                /* TODO: Do we need to change anything in the user data? Not sure if the user data is
251                 *        just encoded using GSM encoding, or it is an actual GSM submit PDU embedded
252                 *        in the user data?
253                 */
254
255        }
256
257        private static final byte TP_MIT_DELIVER       = 0x00; // bit 0 and 1
258        private static final byte TP_MMS_NO_MORE       = 0x04; // bit 2
259        private static final byte TP_RP_NO_REPLY_PATH  = 0x00; // bit 7
260        private static final byte TP_UDHI_MASK         = 0x40; // bit 6
261        private static final byte TP_SRI_NO_REPORT     = 0x00; // bit 5
262
263        private int gsmSubmitGetTpPidOffset() {
264            /* calculate the offset to TP_PID.
265             * The TP-DA has variable length, and the length excludes the 2 byte length and type headers.
266             * The TP-DA is two bytes within the PDU */
267            int offset = 2 + ((data[2]+1) & 0xff)/2 + 2; // data[2] is the number of semi-octets in the phone number (ceil result)
268            if((offset > data.length) || (offset > (2 + 12))) // max length of TP_DA is 12 bytes + two byte offset.
269                throw new IllegalArgumentException("wrongly formatted gsm submit PDU. offset = " + offset);
270            return offset;
271        }
272
273        public int gsmSubmitGetTpDcs() {
274            return data[gsmSubmitGetTpDcsOffset()] & 0xff;
275        }
276
277        public boolean gsmSubmitHasUserDataHeader() {
278            return ((data[0] & 0xff) & TP_UDHI_MASK) == TP_UDHI_MASK;
279        }
280
281        private int gsmSubmitGetTpDcsOffset() {
282            return gsmSubmitGetTpPidOffset() + 1;
283        }
284
285        private int gsmSubmitGetTpUdlOffset() {
286            switch(((data[0]  & 0xff) & (0x08 | 0x04))>>2) {
287            case 0: // Not TP-VP present
288                return gsmSubmitGetTpPidOffset() + 2;
289            case 1: // TP-VP relative format
290                return gsmSubmitGetTpPidOffset() + 2 + 1;
291            case 2: // TP-VP enhanced format
292            case 3: // TP-VP absolute format
293                break;
294            }
295            return gsmSubmitGetTpPidOffset() + 2 + 7;
296        }
297        private int gsmSubmitGetTpUdOffset() {
298            return gsmSubmitGetTpUdlOffset() + 1;
299        }
300
301        public void gsmDecodeUserDataHeader() {
302            ByteArrayInputStream pdu = new ByteArrayInputStream(data);
303
304            pdu.skip(gsmSubmitGetTpUdlOffset());
305            int userDataLength = pdu.read();
306            if(gsmSubmitHasUserDataHeader() == true) {
307                int userDataHeaderLength = pdu.read();
308
309                // This part is only needed to extract the language info, hence only needed for 7 bit encoding
310                if(encoding == SmsConstants.ENCODING_7BIT)
311                {
312                    byte[] udh = new byte[userDataHeaderLength];
313                    try {
314                        pdu.read(udh);
315                    } catch (IOException e) {
316                        Log.w(TAG, "unable to read userDataHeader", e);
317                    }
318                    SmsHeader userDataHeader = SmsHeader.fromByteArray(udh);
319                    languageTable = userDataHeader.languageTable;
320                    languageShiftTable = userDataHeader.languageShiftTable;
321
322                    int headerBits = (userDataHeaderLength + 1) * 8;
323                    int headerSeptets = headerBits / 7;
324                    headerSeptets += (headerBits % 7) > 0 ? 1 : 0;
325                    userDataSeptetPadding = (headerSeptets * 7) - headerBits;
326                    msgSeptetCount = userDataLength - headerSeptets;
327                }
328                userDataMsgOffset = gsmSubmitGetTpUdOffset() + userDataHeaderLength + 1; // Add the byte containing the length
329            }
330            else
331            {
332                userDataSeptetPadding = 0;
333                msgSeptetCount = userDataLength;
334                userDataMsgOffset = gsmSubmitGetTpUdOffset();
335            }
336            if(V) {
337                Log.v(TAG, "encoding:" + encoding);
338                Log.v(TAG, "msgSeptetCount:" + msgSeptetCount);
339                Log.v(TAG, "userDataSeptetPadding:" + userDataSeptetPadding);
340                Log.v(TAG, "languageShiftTable:" + languageShiftTable);
341                Log.v(TAG, "languageTable:" + languageTable);
342                Log.v(TAG, "userDataMsgOffset:" + userDataMsgOffset);
343            }
344        }
345
346        private void gsmWriteDate(ByteArrayOutputStream header, long time) throws UnsupportedEncodingException {
347            SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss");
348            Date date = new Date(time);
349            String timeStr = format.format(date); // Format to YYMMDDTHHMMSS UTC time
350            if(V) Log.v(TAG, "Generated time string: " + timeStr);
351            byte[] timeChars = timeStr.getBytes("US-ASCII");
352
353            for(int i = 0, n = timeStr.length(); i < n; i+=2) {
354                header.write((timeChars[i+1]-0x30) << 4 | (timeChars[i]-0x30)); // Offset from ascii char to decimal value
355            }
356
357            Calendar cal = Calendar.getInstance();
358            int offset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / (15 * 60 * 1000); /* offset in quarters of an hour */
359            String offsetString;
360            if(offset < 0) {
361                offsetString = String.format("%1$02d", -(offset));
362                char[] offsetChars = offsetString.toCharArray();
363                header.write((offsetChars[1]-0x30) << 4 | 0x40 | (offsetChars[0]-0x30));
364            }
365            else {
366                offsetString = String.format("%1$02d", offset);
367                char[] offsetChars = offsetString.toCharArray();
368                header.write((offsetChars[1]-0x30) << 4 | (offsetChars[0]-0x30));
369            }
370        }
371
372/*        private void gsmSubmitExtractUserData() {
373            int userDataLength = data[gsmSubmitGetTpUdlOffset()];
374            userData = new byte[userDataLength];
375            System.arraycopy(userData, 0, data, gsmSubmitGetTpUdOffset(), userDataLength);
376
377        }*/
378
379        /**
380         * Change the GSM Submit Pdu data in this object to a deliver PDU:
381         *  - Build the new header with deliver PDU type, originator and time stamp.
382         *  - Extract encoding details from the submit PDU
383         *  - Extract user data length and user data from the submitPdu
384         *  - Build the new PDU
385         * @param date the time stamp to include (The value is the number of milliseconds since Jan. 1, 1970 GMT.)
386         * @param originator the phone number to include in the deliver PDU header. Any undesired characters,
387         *                    such as '-' will be striped from this string.
388         */
389        public void gsmChangeToDeliverPdu(long date, String originator)
390        {
391            ByteArrayOutputStream newPdu = new ByteArrayOutputStream(22); // 22 is the max length of the deliver pdu header
392            byte[] encodedAddress;
393            int userDataLength = 0;
394            try {
395                newPdu.write(TP_MIT_DELIVER | TP_MMS_NO_MORE | TP_RP_NO_REPLY_PATH | TP_SRI_NO_REPORT
396                             | (data[0] & 0xff)  & TP_UDHI_MASK);
397                encodedAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(originator);
398                if(encodedAddress != null) {
399                    int padding = (encodedAddress[encodedAddress.length-1] & 0xf0) == 0xf0 ? 1 : 0;
400                    encodedAddress[0] = (byte)((encodedAddress[0]-1)*2 - padding); // Convert from octet length to semi octet length
401                    // Insert originator address into the header - this includes the length
402                    newPdu.write(encodedAddress);
403                } else {
404                    newPdu.write(0);    /* zero length */
405                    newPdu.write(0x81); /* International type */
406                }
407
408                newPdu.write(data[gsmSubmitGetTpPidOffset()]);
409                newPdu.write(data[gsmSubmitGetTpDcsOffset()]);
410                // Generate service center time stamp
411                gsmWriteDate(newPdu, date);
412                userDataLength = (data[gsmSubmitGetTpUdlOffset()] & 0xff);
413                newPdu.write(userDataLength);
414                // Copy the pdu user data - keep in mind that the userDataLength is not the length in bytes for 7-bit encoding.
415                newPdu.write(data, gsmSubmitGetTpUdOffset(), data.length - gsmSubmitGetTpUdOffset());
416            } catch (IOException e) {
417                Log.e(TAG, "", e);
418                throw new IllegalArgumentException("Failed to change type to deliver PDU.");
419            }
420            data = newPdu.toByteArray();
421        }
422
423        /* SMS encoding to bmessage strings */
424        /** get the encoding type as a bMessage string */
425        public String getEncodingString(){
426            if(type == SMS_TYPE_GSM)
427            {
428                switch(encoding){
429                case SmsMessage.ENCODING_7BIT:
430                    if(languageTable == 0)
431                        return "G-7BIT";
432                    else
433                        return "G-7BITEXT";
434                case SmsMessage.ENCODING_8BIT:
435                    return "G-8BIT";
436                case SmsMessage.ENCODING_16BIT:
437                    return "G-16BIT";
438                case SmsMessage.ENCODING_UNKNOWN:
439                    default:
440                    return "";
441                }
442            } else /* SMS_TYPE_CDMA */ {
443                switch(encoding){
444                case SmsMessage.ENCODING_7BIT:
445                    return "C-7ASCII";
446                case SmsMessage.ENCODING_8BIT:
447                    return "C-8BIT";
448                case SmsMessage.ENCODING_16BIT:
449                    return "C-UNICODE";
450                case SmsMessage.ENCODING_KSC5601:
451                    return "C-KOREAN";
452                case SmsMessage.ENCODING_UNKNOWN:
453                    default:
454                    return "";
455                }
456            }
457        }
458    }
459
460    private static int sConcatenatedRef = new Random().nextInt(256);
461
462    protected static int getNextConcatenatedRef() {
463        sConcatenatedRef += 1;
464        return sConcatenatedRef;
465    }
466    public static ArrayList<SmsPdu> getSubmitPdus(String messageText, String address){
467        /* Use the generic GSM/CDMA SMS Message functionality within Android to generate the
468         * SMS PDU's as once generated to send the SMS message.
469         */
470
471        int activePhone = TelephonyManager.getDefault().getCurrentPhoneType(); // TODO: Change to use: ((TelephonyManager)myContext.getSystemService(Context.TELEPHONY_SERVICE))
472        int phoneType;
473        GsmAlphabet.TextEncodingDetails ted = (PHONE_TYPE_CDMA == activePhone) ?
474            com.android.internal.telephony.cdma.SmsMessage.calculateLength((CharSequence)messageText, false) :
475            com.android.internal.telephony.gsm.SmsMessage.calculateLength((CharSequence)messageText, false);
476
477        SmsPdu newPdu;
478        String destinationAddress;
479        int msgCount = ted.msgCount;
480        int encoding;
481        int languageTable;
482        int languageShiftTable;
483        int refNumber = getNextConcatenatedRef() & 0x00FF;
484        ArrayList<String> smsFragments = SmsMessage.fragmentText(messageText);
485        ArrayList<SmsPdu> pdus = new ArrayList<SmsPdu>(msgCount);
486        byte[] data;
487
488        // Default to GSM, as this code should not be used, if we neither have CDMA not GSM.
489        phoneType = (activePhone == PHONE_TYPE_CDMA) ? SMS_TYPE_CDMA : SMS_TYPE_GSM;
490        encoding = ted.codeUnitSize;
491        languageTable = ted.languageTable;
492        languageShiftTable = ted.languageShiftTable;
493        destinationAddress = PhoneNumberUtils.stripSeparators(address);
494        if(destinationAddress == null || destinationAddress.length() < 2) {
495            destinationAddress = "12"; // Ensure we add a number at least 2 digits as specified in the GSM spec.
496        }
497
498        if(msgCount == 1){
499            data = SmsMessage.getSubmitPdu(null, destinationAddress, smsFragments.get(0), false).encodedMessage;
500            newPdu = new SmsPdu(data, encoding, phoneType, languageTable);
501            pdus.add(newPdu);
502        }
503        else
504        {
505            /* This code is a reduced copy of the actual code used in the Android SMS sub system,
506             * hence the comments have been left untouched. */
507            for(int i = 0; i < msgCount; i++){
508                SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
509                concatRef.refNumber = refNumber;
510                concatRef.seqNumber = i + 1;  // 1-based sequence
511                concatRef.msgCount = msgCount;
512                // We currently set this to true since our messaging app will never
513                // send more than 255 parts (it converts the message to MMS well before that).
514                // However, we should support 3rd party messaging apps that might need 16-bit
515                // references
516                // Note:  It's not sufficient to just flip this bit to true; it will have
517                // ripple effects (several calculations assume 8-bit ref).
518                concatRef.isEightBits = true;
519                SmsHeader smsHeader = new SmsHeader();
520                smsHeader.concatRef = concatRef;
521
522                /* Depending on the type, call either GSM or CDMA getSubmitPdu(). The encoding
523                 * will be determined(again) by getSubmitPdu().
524                 * All packets need to be encoded using the same encoding, as the bMessage
525                 * only have one filed to describe the encoding for all messages in a concatenated
526                 * SMS... */
527                if (encoding == SmsConstants.ENCODING_7BIT) {
528                    smsHeader.languageTable = languageTable;
529                    smsHeader.languageShiftTable = languageShiftTable;
530                }
531
532                if(phoneType == SMS_TYPE_GSM){
533                    data = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(null, destinationAddress,
534                            smsFragments.get(i), false, SmsHeader.toByteArray(smsHeader),
535                            encoding, languageTable, languageShiftTable).encodedMessage;
536                } else { // SMS_TYPE_CDMA
537                    UserData uData = new UserData();
538                    uData.payloadStr = smsFragments.get(i);
539                    uData.userDataHeader = smsHeader;
540                    if (encoding == SmsConstants.ENCODING_7BIT) {
541                        uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
542                    } else { // assume UTF-16
543                        uData.msgEncoding = UserData.ENCODING_UNICODE_16;
544                    }
545                    uData.msgEncodingSet = true;
546                    data = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(destinationAddress,
547                            uData, false).encodedMessage;
548                }
549                newPdu = new SmsPdu(data, encoding, phoneType, languageTable);
550                pdus.add(newPdu);
551            }
552        }
553
554        return pdus;
555    }
556
557    /**
558     * Generate a list of deliver PDUs. The messageText and address parameters must be different from null,
559     * for CDMA the date can be omitted (and will be ignored if supplied)
560     * @param messageText The text to include.
561     * @param address The originator address.
562     * @param date The delivery time stamp.
563     * @return
564     */
565    public static ArrayList<SmsPdu> getDeliverPdus(String messageText, String address, long date){
566        ArrayList<SmsPdu> deliverPdus = getSubmitPdus(messageText, address);
567
568        /*
569         * For CDMA the only difference between deliver and submit pdus are the messageType,
570         * which is set in encodeMessageId, (the higher 4 bits of the 1st byte
571         * of the Message identification sub parameter data.) and the address type.
572         *
573         * For GSM, a larger part of the header needs to be generated.
574         */
575        for(SmsPdu currentPdu : deliverPdus){
576            if(currentPdu.getType() == SMS_TYPE_CDMA){
577                currentPdu.cdmaChangeToDeliverPdu(date);
578            } else { /* SMS_TYPE_GSM */
579                currentPdu.gsmChangeToDeliverPdu(date, address);
580            }
581        }
582
583        return deliverPdus;
584    }
585
586    public static void testSendRawPdu(SmsPdu pdu){
587        if(pdu.getType() == SMS_TYPE_CDMA){
588            /* TODO: Try to send the message using SmsManager.sendData()?*/
589        }else {
590
591        }
592    }
593
594    /**
595     * The decoding only supports decoding the actual textual content of the PDU received
596     * from the MAP client. (As the Android system has no interface to send pre encoded PDUs)
597     * The destination address must be extracted from the bmessage vCard(s).
598     */
599    public static String decodePdu(byte[] data, int type) {
600        String ret;
601        if(type == SMS_TYPE_CDMA) {
602            /* This is able to handle both submit and deliver PDUs */
603            ret = com.android.internal.telephony.cdma.SmsMessage.createFromEfRecord(0, data).getMessageBody();
604        } else {
605            /* For GSM, there is no submit pdu decoder, and most parser utils are private, and only minded for submit pdus */
606            ret = gsmParseSubmitPdu(data);
607        }
608        return ret;
609    }
610
611    /* At the moment we do not support using a SC-address. Use this function to strip off
612     * the SC-address before parsing it to the SmsPdu. (this was added in errata 4335)
613     */
614    private static byte[] gsmStripOffScAddress(byte[] data) {
615        /* The format of a native GSM SMS is: <sc-address><pdu> where sc-address is:
616         * <length-byte><type-byte><number-bytes> */
617        int addressLength = data[0] & 0xff; // Treat the byte value as an unsigned value
618        if(addressLength >= data.length) // We could verify that the address-length is no longer than 11 bytes
619            throw new IllegalArgumentException("Length of address exeeds the length of the PDU data.");
620        int pduLength = data.length-(1+addressLength);
621        byte[] newData = new byte[pduLength];
622        System.arraycopy(data, 1+addressLength, newData, 0, pduLength);
623        return newData;
624    }
625
626    private static String gsmParseSubmitPdu(byte[] data) {
627        /* Things to do:
628         *  - extract hasUsrData bit
629         *  - extract TP-DCS -> Character set, compressed etc.
630         *  - extract user data header to get the language properties
631         *  - extract user data
632         *  - decode the string */
633        //Strip off the SC-address before parsing
634        SmsPdu pdu = new SmsPdu(gsmStripOffScAddress(data), SMS_TYPE_GSM);
635        boolean userDataCompressed = false;
636        int dataCodingScheme = pdu.gsmSubmitGetTpDcs();
637        int encodingType =  SmsConstants.ENCODING_UNKNOWN;
638        String messageBody = null;
639
640        // Look up the data encoding scheme
641        if ((dataCodingScheme & 0x80) == 0) {
642            // Bits 7..4 == 0xxx
643            userDataCompressed = (0 != (dataCodingScheme & 0x20));
644
645            if (userDataCompressed) {
646                Log.w(TAG, "4 - Unsupported SMS data coding scheme "
647                        + "(compression) " + (dataCodingScheme & 0xff));
648            } else {
649                switch ((dataCodingScheme >> 2) & 0x3) {
650                case 0: // GSM 7 bit default alphabet
651                    encodingType =  SmsConstants.ENCODING_7BIT;
652                    break;
653
654                case 2: // UCS 2 (16bit)
655                    encodingType =  SmsConstants.ENCODING_16BIT;
656                    break;
657
658                case 1: // 8 bit data
659                case 3: // reserved
660                    Log.w(TAG, "1 - Unsupported SMS data coding scheme "
661                            + (dataCodingScheme & 0xff));
662                    encodingType =  SmsConstants.ENCODING_8BIT;
663                    break;
664                }
665            }
666        } else if ((dataCodingScheme & 0xf0) == 0xf0) {
667            userDataCompressed = false;
668
669            if (0 == (dataCodingScheme & 0x04)) {
670                // GSM 7 bit default alphabet
671                encodingType =  SmsConstants.ENCODING_7BIT;
672            } else {
673                // 8 bit data
674                encodingType =  SmsConstants.ENCODING_8BIT;
675            }
676        } else if ((dataCodingScheme & 0xF0) == 0xC0
677                || (dataCodingScheme & 0xF0) == 0xD0
678                || (dataCodingScheme & 0xF0) == 0xE0) {
679            // 3GPP TS 23.038 V7.0.0 (2006-03) section 4
680
681            // 0xC0 == 7 bit, don't store
682            // 0xD0 == 7 bit, store
683            // 0xE0 == UCS-2, store
684
685            if ((dataCodingScheme & 0xF0) == 0xE0) {
686                encodingType =  SmsConstants.ENCODING_16BIT;
687            } else {
688                encodingType =  SmsConstants.ENCODING_7BIT;
689            }
690
691            userDataCompressed = false;
692
693            // bit 0x04 reserved
694        } else if ((dataCodingScheme & 0xC0) == 0x80) {
695            // 3GPP TS 23.038 V7.0.0 (2006-03) section 4
696            // 0x80..0xBF == Reserved coding groups
697            if (dataCodingScheme == 0x84) {
698                // This value used for KSC5601 by carriers in Korea.
699                encodingType =  SmsConstants.ENCODING_KSC5601;
700            } else {
701                Log.w(TAG, "5 - Unsupported SMS data coding scheme "
702                        + (dataCodingScheme & 0xff));
703            }
704        } else {
705            Log.w(TAG, "3 - Unsupported SMS data coding scheme "
706                    + (dataCodingScheme & 0xff));
707        }
708
709        /* TODO: This is NOT good design - to have the pdu class being depending on these two function calls.
710         *        - move the encoding extraction into the pdu class */
711        pdu.setEncoding(encodingType);
712        pdu.gsmDecodeUserDataHeader();
713
714        try {
715            switch (encodingType) {
716            case  SmsConstants.ENCODING_UNKNOWN:
717            case  SmsConstants.ENCODING_8BIT:
718                Log.w(TAG, "Unknown encoding type: " + encodingType);
719                messageBody = null;
720                break;
721
722            case  SmsConstants.ENCODING_7BIT:
723                messageBody = GsmAlphabet.gsm7BitPackedToString(pdu.getData(), pdu.getUserDataMsgOffset(),
724                                pdu.getMsgSeptetCount(), pdu.getUserDataSeptetPadding(), pdu.getLanguageTable(),
725                                pdu.getLanguageShiftTable());
726                Log.i(TAG, "Decoded as 7BIT: " + messageBody);
727
728                break;
729
730            case  SmsConstants.ENCODING_16BIT:
731                messageBody = new String(pdu.getData(), pdu.getUserDataMsgOffset(), pdu.getUserDataMsgSize(), "utf-16");
732                Log.i(TAG, "Decoded as 16BIT: " + messageBody);
733                break;
734
735            case SmsConstants.ENCODING_KSC5601:
736                messageBody = new String(pdu.getData(), pdu.getUserDataMsgOffset(), pdu.getUserDataMsgSize(), "KSC5601");
737                Log.i(TAG, "Decoded as KSC5601: " + messageBody);
738                break;
739            }
740        } catch (UnsupportedEncodingException e) {
741            Log.e(TAG, "Unsupported encoding type???", e); // This should never happen.
742            return null;
743        }
744
745        return messageBody;
746    }
747
748}
749