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 17 package com.android.internal.telephony.gsm; 18 19 import android.telephony.SmsCbLocation; 20 import android.telephony.SmsCbMessage; 21 import android.util.Pair; 22 23 import com.android.internal.telephony.GsmAlphabet; 24 import com.android.internal.telephony.SmsConstants; 25 26 import 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 */ 32 public 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 static SmsCbMessage createSmsCbMessage(SmsCbHeader header, SmsCbLocation location, 63 byte[][] pdus) throws IllegalArgumentException { 64 if (header.isEtwsPrimaryNotification()) { 65 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 66 header.getGeographicalScope(), header.getSerialNumber(), 67 location, header.getServiceCategory(), 68 null, "ETWS", SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, 69 header.getEtwsInfo(), header.getCmasInfo()); 70 } else { 71 String language = null; 72 StringBuilder sb = new StringBuilder(); 73 for (byte[] pdu : pdus) { 74 Pair<String, String> p = parseBody(header, pdu); 75 language = p.first; 76 sb.append(p.second); 77 } 78 int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY 79 : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; 80 81 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 82 header.getGeographicalScope(), header.getSerialNumber(), location, 83 header.getServiceCategory(), language, sb.toString(), priority, 84 header.getEtwsInfo(), header.getCmasInfo()); 85 } 86 } 87 88 /** 89 * Create a new SmsCbMessage object from one or more received PDUs. This is used by some 90 * CellBroadcastReceiver test cases, because SmsCbHeader is now package local. 91 * 92 * @param location the location (geographical scope) for the message 93 * @param pdus PDU bytes 94 */ 95 public static SmsCbMessage createSmsCbMessage(SmsCbLocation location, byte[][] pdus) 96 throws IllegalArgumentException { 97 SmsCbHeader header = new SmsCbHeader(pdus[0]); 98 return createSmsCbMessage(header, location, pdus); 99 } 100 101 /** 102 * Parse and unpack the body text according to the encoding in the DCS. 103 * After completing successfully this method will have assigned the body 104 * text into mBody, and optionally the language code into mLanguage 105 * 106 * @param header the message header to use 107 * @param pdu the PDU to decode 108 * @return a Pair of Strings containing the language and body of the message 109 */ 110 private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) { 111 int encoding; 112 String language = null; 113 boolean hasLanguageIndicator = false; 114 int dataCodingScheme = header.getDataCodingScheme(); 115 116 // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, 117 // section 5. 118 switch ((dataCodingScheme & 0xf0) >> 4) { 119 case 0x00: 120 encoding = SmsConstants.ENCODING_7BIT; 121 language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f]; 122 break; 123 124 case 0x01: 125 hasLanguageIndicator = true; 126 if ((dataCodingScheme & 0x0f) == 0x01) { 127 encoding = SmsConstants.ENCODING_16BIT; 128 } else { 129 encoding = SmsConstants.ENCODING_7BIT; 130 } 131 break; 132 133 case 0x02: 134 encoding = SmsConstants.ENCODING_7BIT; 135 language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f]; 136 break; 137 138 case 0x03: 139 encoding = SmsConstants.ENCODING_7BIT; 140 break; 141 142 case 0x04: 143 case 0x05: 144 switch ((dataCodingScheme & 0x0c) >> 2) { 145 case 0x01: 146 encoding = SmsConstants.ENCODING_8BIT; 147 break; 148 149 case 0x02: 150 encoding = SmsConstants.ENCODING_16BIT; 151 break; 152 153 case 0x00: 154 default: 155 encoding = SmsConstants.ENCODING_7BIT; 156 break; 157 } 158 break; 159 160 case 0x06: 161 case 0x07: 162 // Compression not supported 163 case 0x09: 164 // UDH structure not supported 165 case 0x0e: 166 // Defined by the WAP forum not supported 167 throw new IllegalArgumentException("Unsupported GSM dataCodingScheme " 168 + dataCodingScheme); 169 170 case 0x0f: 171 if (((dataCodingScheme & 0x04) >> 2) == 0x01) { 172 encoding = SmsConstants.ENCODING_8BIT; 173 } else { 174 encoding = SmsConstants.ENCODING_7BIT; 175 } 176 break; 177 178 default: 179 // Reserved values are to be treated as 7-bit 180 encoding = SmsConstants.ENCODING_7BIT; 181 break; 182 } 183 184 if (header.isUmtsFormat()) { 185 // Payload may contain multiple pages 186 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 187 188 if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) 189 * nrPages) { 190 throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " 191 + nrPages + " pages"); 192 } 193 194 StringBuilder sb = new StringBuilder(); 195 196 for (int i = 0; i < nrPages; i++) { 197 // Each page is 82 bytes followed by a length octet indicating 198 // the number of useful octets within those 82 199 int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; 200 int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; 201 202 if (length > PDU_BODY_PAGE_LENGTH) { 203 throw new IllegalArgumentException("Page length " + length 204 + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); 205 } 206 207 Pair<String, String> p = unpackBody(pdu, encoding, offset, length, 208 hasLanguageIndicator, language); 209 language = p.first; 210 sb.append(p.second); 211 } 212 return new Pair<String, String>(language, sb.toString()); 213 } else { 214 // Payload is one single page 215 int offset = SmsCbHeader.PDU_HEADER_LENGTH; 216 int length = pdu.length - offset; 217 218 return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language); 219 } 220 } 221 222 /** 223 * Unpack body text from the pdu using the given encoding, position and 224 * length within the pdu 225 * 226 * @param pdu The pdu 227 * @param encoding The encoding, as derived from the DCS 228 * @param offset Position of the first byte to unpack 229 * @param length Number of bytes to unpack 230 * @param hasLanguageIndicator true if the body text is preceded by a 231 * language indicator. If so, this method will as a side-effect 232 * assign the extracted language code into mLanguage 233 * @param language the language to return if hasLanguageIndicator is false 234 * @return a Pair of Strings containing the language and body of the message 235 */ 236 private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length, 237 boolean hasLanguageIndicator, String language) { 238 String body = null; 239 240 switch (encoding) { 241 case SmsConstants.ENCODING_7BIT: 242 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); 243 244 if (hasLanguageIndicator && body != null && body.length() > 2) { 245 // Language is two GSM characters followed by a CR. 246 // The actual body text is offset by 3 characters. 247 language = body.substring(0, 2); 248 body = body.substring(3); 249 } 250 break; 251 252 case SmsConstants.ENCODING_16BIT: 253 if (hasLanguageIndicator && pdu.length >= offset + 2) { 254 // Language is two GSM characters. 255 // The actual body text is offset by 2 bytes. 256 language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); 257 offset += 2; 258 length -= 2; 259 } 260 261 try { 262 body = new String(pdu, offset, (length & 0xfffe), "utf-16"); 263 } catch (UnsupportedEncodingException e) { 264 // Apparently it wasn't valid UTF-16. 265 throw new IllegalArgumentException("Error decoding UTF-16 message", e); 266 } 267 break; 268 269 default: 270 break; 271 } 272 273 if (body != null) { 274 // Remove trailing carriage return 275 for (int i = body.length() - 1; i >= 0; i--) { 276 if (body.charAt(i) != CARRIAGE_RETURN) { 277 body = body.substring(0, i + 1); 278 break; 279 } 280 } 281 } else { 282 body = ""; 283 } 284 285 return new Pair<String, String>(language, body); 286 } 287 } 288