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