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