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