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