1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.internal.telephony.gsm; 18 19import android.telephony.SmsCbLocation; 20import android.telephony.SmsCbMessage; 21import android.util.Pair; 22 23import com.android.internal.telephony.GsmAlphabet; 24import com.android.internal.telephony.SmsConstants; 25 26import java.io.UnsupportedEncodingException; 27 28/** 29 * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is 30 * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases. 31 */ 32public class GsmSmsCbMessage { 33 34 /** 35 * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. 36 */ 37 private static final String[] LANGUAGE_CODES_GROUP_0 = { 38 "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu", 39 "pl", null 40 }; 41 42 /** 43 * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. 44 */ 45 private static final String[] LANGUAGE_CODES_GROUP_2 = { 46 "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null, 47 null, null 48 }; 49 50 private static final char CARRIAGE_RETURN = 0x0d; 51 52 private static final int PDU_BODY_PAGE_LENGTH = 82; 53 54 /** Utility class with only static methods. */ 55 private GsmSmsCbMessage() { } 56 57 /** 58 * Create a new SmsCbMessage object from a header object plus one or more received PDUs. 59 * 60 * @param pdus PDU bytes 61 */ 62 public static SmsCbMessage createSmsCbMessage(SmsCbHeader header, SmsCbLocation location, 63 byte[][] pdus) throws IllegalArgumentException { 64 if (header.isEtwsPrimaryNotification()) { 65 // ETSI TS 23.041 ETWS Primary Notification message 66 // ETWS primary message only contains 4 fields including serial number, 67 // message identifier, warning type, and warning security information. 68 // There is no field for the content/text. We hardcode "ETWS" in the 69 // text body so the user won't see an empty dialog without any text. 70 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 71 header.getGeographicalScope(), header.getSerialNumber(), 72 location, header.getServiceCategory(), 73 null, "ETWS", SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, 74 header.getEtwsInfo(), header.getCmasInfo()); 75 } else { 76 String language = null; 77 StringBuilder sb = new StringBuilder(); 78 for (byte[] pdu : pdus) { 79 Pair<String, String> p = parseBody(header, pdu); 80 language = p.first; 81 sb.append(p.second); 82 } 83 int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY 84 : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; 85 86 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 87 header.getGeographicalScope(), header.getSerialNumber(), location, 88 header.getServiceCategory(), language, sb.toString(), priority, 89 header.getEtwsInfo(), header.getCmasInfo()); 90 } 91 } 92 93 /** 94 * Create a new SmsCbMessage object from one or more received PDUs. This is used by some 95 * CellBroadcastReceiver test cases, because SmsCbHeader is now package local. 96 * 97 * @param location the location (geographical scope) for the message 98 * @param pdus PDU bytes 99 */ 100 public static SmsCbMessage createSmsCbMessage(SmsCbLocation location, byte[][] pdus) 101 throws IllegalArgumentException { 102 SmsCbHeader header = new SmsCbHeader(pdus[0]); 103 return createSmsCbMessage(header, location, pdus); 104 } 105 106 /** 107 * Parse and unpack the body text according to the encoding in the DCS. 108 * After completing successfully this method will have assigned the body 109 * text into mBody, and optionally the language code into mLanguage 110 * 111 * @param header the message header to use 112 * @param pdu the PDU to decode 113 * @return a Pair of Strings containing the language and body of the message 114 */ 115 private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) { 116 int encoding; 117 String language = null; 118 boolean hasLanguageIndicator = false; 119 int dataCodingScheme = header.getDataCodingScheme(); 120 121 // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, 122 // section 5. 123 switch ((dataCodingScheme & 0xf0) >> 4) { 124 case 0x00: 125 encoding = SmsConstants.ENCODING_7BIT; 126 language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f]; 127 break; 128 129 case 0x01: 130 hasLanguageIndicator = true; 131 if ((dataCodingScheme & 0x0f) == 0x01) { 132 encoding = SmsConstants.ENCODING_16BIT; 133 } else { 134 encoding = SmsConstants.ENCODING_7BIT; 135 } 136 break; 137 138 case 0x02: 139 encoding = SmsConstants.ENCODING_7BIT; 140 language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f]; 141 break; 142 143 case 0x03: 144 encoding = SmsConstants.ENCODING_7BIT; 145 break; 146 147 case 0x04: 148 case 0x05: 149 switch ((dataCodingScheme & 0x0c) >> 2) { 150 case 0x01: 151 encoding = SmsConstants.ENCODING_8BIT; 152 break; 153 154 case 0x02: 155 encoding = SmsConstants.ENCODING_16BIT; 156 break; 157 158 case 0x00: 159 default: 160 encoding = SmsConstants.ENCODING_7BIT; 161 break; 162 } 163 break; 164 165 case 0x06: 166 case 0x07: 167 // Compression not supported 168 case 0x09: 169 // UDH structure not supported 170 case 0x0e: 171 // Defined by the WAP forum not supported 172 throw new IllegalArgumentException("Unsupported GSM dataCodingScheme " 173 + dataCodingScheme); 174 175 case 0x0f: 176 if (((dataCodingScheme & 0x04) >> 2) == 0x01) { 177 encoding = SmsConstants.ENCODING_8BIT; 178 } else { 179 encoding = SmsConstants.ENCODING_7BIT; 180 } 181 break; 182 183 default: 184 // Reserved values are to be treated as 7-bit 185 encoding = SmsConstants.ENCODING_7BIT; 186 break; 187 } 188 189 if (header.isUmtsFormat()) { 190 // Payload may contain multiple pages 191 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 192 193 if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) 194 * nrPages) { 195 throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " 196 + nrPages + " pages"); 197 } 198 199 StringBuilder sb = new StringBuilder(); 200 201 for (int i = 0; i < nrPages; i++) { 202 // Each page is 82 bytes followed by a length octet indicating 203 // the number of useful octets within those 82 204 int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; 205 int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; 206 207 if (length > PDU_BODY_PAGE_LENGTH) { 208 throw new IllegalArgumentException("Page length " + length 209 + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); 210 } 211 212 Pair<String, String> p = unpackBody(pdu, encoding, offset, length, 213 hasLanguageIndicator, language); 214 language = p.first; 215 sb.append(p.second); 216 } 217 return new Pair<String, String>(language, sb.toString()); 218 } else { 219 // Payload is one single page 220 int offset = SmsCbHeader.PDU_HEADER_LENGTH; 221 int length = pdu.length - offset; 222 223 return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language); 224 } 225 } 226 227 /** 228 * Unpack body text from the pdu using the given encoding, position and 229 * length within the pdu 230 * 231 * @param pdu The pdu 232 * @param encoding The encoding, as derived from the DCS 233 * @param offset Position of the first byte to unpack 234 * @param length Number of bytes to unpack 235 * @param hasLanguageIndicator true if the body text is preceded by a 236 * language indicator. If so, this method will as a side-effect 237 * assign the extracted language code into mLanguage 238 * @param language the language to return if hasLanguageIndicator is false 239 * @return a Pair of Strings containing the language and body of the message 240 */ 241 private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length, 242 boolean hasLanguageIndicator, String language) { 243 String body = null; 244 245 switch (encoding) { 246 case SmsConstants.ENCODING_7BIT: 247 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); 248 249 if (hasLanguageIndicator && body != null && body.length() > 2) { 250 // Language is two GSM characters followed by a CR. 251 // The actual body text is offset by 3 characters. 252 language = body.substring(0, 2); 253 body = body.substring(3); 254 } 255 break; 256 257 case SmsConstants.ENCODING_16BIT: 258 if (hasLanguageIndicator && pdu.length >= offset + 2) { 259 // Language is two GSM characters. 260 // The actual body text is offset by 2 bytes. 261 language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); 262 offset += 2; 263 length -= 2; 264 } 265 266 try { 267 body = new String(pdu, offset, (length & 0xfffe), "utf-16"); 268 } catch (UnsupportedEncodingException e) { 269 // Apparently it wasn't valid UTF-16. 270 throw new IllegalArgumentException("Error decoding UTF-16 message", e); 271 } 272 break; 273 274 default: 275 break; 276 } 277 278 if (body != null) { 279 // Remove trailing carriage return 280 for (int i = body.length() - 1; i >= 0; i--) { 281 if (body.charAt(i) != CARRIAGE_RETURN) { 282 body = body.substring(0, i + 1); 283 break; 284 } 285 } 286 } else { 287 body = ""; 288 } 289 290 return new Pair<String, String>(language, body); 291 } 292} 293