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