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.cellbroadcastreceiver.tests; 18 19 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE; 20 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI; 21 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY; 22 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE; 23 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI; 24 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.telephony.CbGeoUtils.Circle; 28 import android.telephony.CbGeoUtils.Geometry; 29 import android.telephony.CbGeoUtils.LatLng; 30 import android.telephony.CbGeoUtils.Polygon; 31 import android.telephony.Rlog; 32 import android.telephony.SmsCbLocation; 33 import android.telephony.SmsCbMessage; 34 import android.telephony.SubscriptionManager; 35 import android.util.Pair; 36 37 import com.android.cellbroadcastservice.CbGeoUtils; 38 import com.android.cellbroadcastservice.GsmAlphabet; 39 import com.android.cellbroadcastservice.SmsCbHeader; 40 import com.android.cellbroadcastservice.SmsCbHeader.DataCodingScheme; 41 import com.android.internal.telephony.SmsConstants; 42 import com.android.modules.utils.build.SdkLevel; 43 44 import java.io.UnsupportedEncodingException; 45 import java.util.ArrayList; 46 import java.util.List; 47 48 /** 49 * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is 50 * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases. 51 */ 52 public class GsmSmsCbMessage { 53 private static final String TAG = GsmSmsCbMessage.class.getSimpleName(); 54 55 private static final char CARRIAGE_RETURN = 0x0d; 56 57 private static final int PDU_BODY_PAGE_LENGTH = 82; 58 59 /** Utility class with only static methods. */ GsmSmsCbMessage()60 private GsmSmsCbMessage() { } 61 62 /** 63 * Get built-in ETWS primary messages by category. ETWS primary message does not contain text, 64 * so we have to show the pre-built messages to the user. 65 * 66 * @param context Device context 67 * @param category ETWS message category defined in SmsCbConstants 68 * @return ETWS text message in string. Return an empty string if no match. 69 */ getEtwsPrimaryMessage(Context context, int category)70 private static String getEtwsPrimaryMessage(Context context, int category) { 71 final Resources r = context.getResources(); 72 switch (category) { 73 case ETWS_WARNING_TYPE_EARTHQUAKE: 74 return "CBR test app string for earthquake"; 75 case ETWS_WARNING_TYPE_TSUNAMI: 76 return "CBR test app string for tsunami"; 77 case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI: 78 return "CBR test app string for earthquake and tsunami"; 79 case ETWS_WARNING_TYPE_TEST_MESSAGE: 80 return "CBR test app string for test"; 81 case ETWS_WARNING_TYPE_OTHER_EMERGENCY: 82 return "CBR test app string for emergency"; 83 default: 84 return ""; 85 } 86 } 87 88 /** 89 * Create a new SmsCbMessage object from a header object plus one or more received PDUs. 90 * 91 * @param pdus PDU bytes 92 * @slotIndex slotIndex for which received sms cb message 93 */ createSmsCbMessage(Context context, SmsCbHeader header, SmsCbLocation location, byte[][] pdus, int slotIndex)94 public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header, 95 SmsCbLocation location, byte[][] pdus, int slotIndex) 96 throws IllegalArgumentException { 97 int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 98 if (SdkLevel.isAtLeastU()) { 99 subId = SubscriptionManager.getSubscriptionId(slotIndex); 100 } else { 101 SubscriptionManager sm = context.getSystemService(SubscriptionManager.class); 102 int[] subIds = sm.getSubscriptionIds(slotIndex); 103 if (subIds != null && subIds.length > 0) { 104 subId = subIds[0]; 105 } 106 } 107 108 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 109 subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; 110 } 111 112 long receivedTimeMillis = System.currentTimeMillis(); 113 if (header.isEtwsPrimaryNotification()) { 114 // ETSI TS 23.041 ETWS Primary Notification message 115 // ETWS primary message only contains 4 fields including serial number, 116 // message identifier, warning type, and warning security information. 117 // There is no field for the content/text so we get the text from the resources. 118 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(), 119 header.getSerialNumber(), location, header.getServiceCategory(), null, 0, 120 getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()), 121 SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(), 122 header.getCmasInfo(), 0, null /* geometries */, receivedTimeMillis, slotIndex, 123 subId); 124 } else if (header.isUmtsFormat()) { 125 // UMTS format has only 1 PDU 126 byte[] pdu = pdus[0]; 127 Pair<String, String> cbData = parseUmtsBody(header, pdu); 128 String language = cbData.first; 129 String body = cbData.second; 130 int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY 131 : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; 132 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 133 int wacDataOffset = SmsCbHeader.PDU_HEADER_LENGTH 134 + 1 // number of pages 135 + (PDU_BODY_PAGE_LENGTH + 1) * nrPages; // cb data 136 137 // Has Warning Area Coordinates information 138 List<Geometry> geometries = null; 139 int maximumWaitingTimeSec = 255; 140 if (pdu.length > wacDataOffset) { 141 try { 142 Pair<Integer, List<Geometry>> wac = parseWarningAreaCoordinates(pdu, 143 wacDataOffset); 144 maximumWaitingTimeSec = wac.first; 145 geometries = wac.second; 146 } catch (Exception ex) { 147 // Catch the exception here, the message will be considered as having no WAC 148 // information which means the message will be broadcasted directly. 149 Rlog.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString()); 150 } 151 } 152 153 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 154 header.getGeographicalScope(), header.getSerialNumber(), location, 155 header.getServiceCategory(), language, header.getDataCodingScheme(), body, 156 priority, header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec, 157 geometries, receivedTimeMillis, slotIndex, subId); 158 } else { 159 String language = null; 160 StringBuilder sb = new StringBuilder(); 161 for (byte[] pdu : pdus) { 162 Pair<String, String> p = parseGsmBody(header, pdu); 163 language = p.first; 164 sb.append(p.second); 165 } 166 int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY 167 : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; 168 169 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 170 header.getGeographicalScope(), header.getSerialNumber(), location, 171 header.getServiceCategory(), language, header.getDataCodingScheme(), 172 sb.toString(), priority, header.getEtwsInfo(), header.getCmasInfo(), 0, 173 null /* geometries */, receivedTimeMillis, slotIndex, subId); 174 } 175 } 176 177 /** 178 * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV. 179 * 180 * @param pdu Warning Area Coordinates TLV. 181 * @param wacOffset the offset of Warning Area Coordinates TLV. 182 * @return a pair with the first element is maximum wait time and the second is the broadcast 183 * area. The default value of the maximum wait time is 255 which means use the device default 184 * value. 185 */ parseWarningAreaCoordinates( byte[] pdu, int wacOffset)186 private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates( 187 byte[] pdu, int wacOffset) { 188 // little-endian 189 int wacDataLength = ((pdu[wacOffset + 1] & 0xff) << 8) | (pdu[wacOffset] & 0xff); 190 int offset = wacOffset + 2; 191 192 if (offset + wacDataLength > pdu.length) { 193 throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at" 194 + "least " + offset + wacDataLength + ", actual is " + pdu.length); 195 } 196 197 BitStreamReader bitReader = new BitStreamReader(pdu, offset); 198 199 int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET; 200 201 List<Geometry> geo = new ArrayList<>(); 202 int remainedBytes = wacDataLength; 203 while (remainedBytes > 0) { 204 int type = bitReader.read(4); 205 int length = bitReader.read(10); 206 remainedBytes -= length; 207 // Skip the 2 remained bits 208 bitReader.skip(); 209 210 switch (type) { 211 case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME: 212 maximumWaitTimeSec = bitReader.read(8); 213 break; 214 case CbGeoUtils.GEOMETRY_TYPE_POLYGON: 215 List<LatLng> latLngs = new ArrayList<>(); 216 // Each coordinate is represented by 44 bits integer. 217 // ATIS-0700041 5.2.4 Coordinate coding 218 int n = (length - 2) * 8 / 44; 219 for (int i = 0; i < n; i++) { 220 latLngs.add(getLatLng(bitReader)); 221 } 222 // Skip the padding bits 223 bitReader.skip(); 224 geo.add(new Polygon(latLngs)); 225 break; 226 case CbGeoUtils.GEOMETRY_TYPE_CIRCLE: 227 LatLng center = getLatLng(bitReader); 228 // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the 229 // distance unit during geo-fencing. 230 // ATIS-0700041 5.2.5 radius coding 231 double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0; 232 geo.add(new Circle(center, radius)); 233 break; 234 default: 235 throw new IllegalArgumentException("Unsupported geoType = " + type); 236 } 237 } 238 return new Pair(maximumWaitTimeSec, geo); 239 } 240 241 /** 242 * The coordinate is (latitude, longitude), represented by a 44 bits integer. 243 * The coding is defined in ATIS-0700041 5.2.4 244 * @param bitReader 245 * @return coordinate (latitude, longitude) 246 */ getLatLng(BitStreamReader bitReader)247 private static LatLng getLatLng(BitStreamReader bitReader) { 248 // wacLatitude = floor(((latitude + 90) / 180) * 2^22) 249 // wacLongitude = floor(((longitude + 180) / 360) * 2^22) 250 int wacLat = bitReader.read(22); 251 int wacLng = bitReader.read(22); 252 253 // latitude = wacLatitude * 180 / 2^22 - 90 254 // longitude = wacLongitude * 360 / 2^22 -180 255 return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180)); 256 } 257 258 /** 259 * Parse and unpack the UMTS body text according to the encoding in the data coding scheme. 260 * 261 * @param header the message header to use 262 * @param pdu the PDU to decode 263 * @return a pair of string containing the language and body of the message in order 264 */ parseUmtsBody(SmsCbHeader header, byte[] pdu)265 private static Pair<String, String> parseUmtsBody(SmsCbHeader header, byte[] pdu) { 266 // Payload may contain multiple pages 267 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 268 String language = header.getDataCodingSchemeStructedData().language; 269 270 if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) 271 * nrPages) { 272 throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " 273 + nrPages + " pages"); 274 } 275 276 StringBuilder sb = new StringBuilder(); 277 278 for (int i = 0; i < nrPages; i++) { 279 // Each page is 82 bytes followed by a length octet indicating 280 // the number of useful octets within those 82 281 int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; 282 int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; 283 284 if (length > PDU_BODY_PAGE_LENGTH) { 285 throw new IllegalArgumentException("Page length " + length 286 + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); 287 } 288 289 Pair<String, String> p = unpackBody(pdu, offset, length, 290 header.getDataCodingSchemeStructedData()); 291 language = p.first; 292 sb.append(p.second); 293 } 294 return new Pair(language, sb.toString()); 295 296 } 297 298 /** 299 * Parse and unpack the GSM body text according to the encoding in the data coding scheme. 300 * @param header the message header to use 301 * @param pdu the PDU to decode 302 * @return a pair of string containing the language and body of the message in order 303 */ parseGsmBody(SmsCbHeader header, byte[] pdu)304 private static Pair<String, String> parseGsmBody(SmsCbHeader header, byte[] pdu) { 305 // Payload is one single page 306 int offset = SmsCbHeader.PDU_HEADER_LENGTH; 307 int length = pdu.length - offset; 308 return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData()); 309 } 310 311 /** 312 * Unpack body text from the pdu using the given encoding, position and length within the pdu. 313 * 314 * @param pdu The pdu 315 * @param offset Position of the first byte to unpack 316 * @param length Number of bytes to unpack 317 * @param dcs data coding scheme 318 * @return a Pair of Strings containing the language and body of the message 319 */ unpackBody(byte[] pdu, int offset, int length, DataCodingScheme dcs)320 private static Pair<String, String> unpackBody(byte[] pdu, int offset, int length, 321 DataCodingScheme dcs) { 322 String body = null; 323 324 String language = dcs.language; 325 switch (dcs.encoding) { 326 case SmsConstants.ENCODING_7BIT: 327 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); 328 329 if (dcs.hasLanguageIndicator && body != null && body.length() > 2) { 330 // Language is two GSM characters followed by a CR. 331 // The actual body text is offset by 3 characters. 332 language = body.substring(0, 2); 333 body = body.substring(3); 334 } 335 break; 336 337 case SmsConstants.ENCODING_16BIT: 338 if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) { 339 // Language is two GSM characters. 340 // The actual body text is offset by 2 bytes. 341 language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); 342 offset += 2; 343 length -= 2; 344 } 345 346 try { 347 body = new String(pdu, offset, (length & 0xfffe), "utf-16"); 348 } catch (UnsupportedEncodingException e) { 349 // Apparently it wasn't valid UTF-16. 350 throw new IllegalArgumentException("Error decoding UTF-16 message", e); 351 } 352 break; 353 354 default: 355 break; 356 } 357 358 if (body != null) { 359 // Remove trailing carriage return 360 for (int i = body.length() - 1; i >= 0; i--) { 361 if (body.charAt(i) != CARRIAGE_RETURN) { 362 body = body.substring(0, i + 1); 363 break; 364 } 365 } 366 } else { 367 body = ""; 368 } 369 370 return new Pair<String, String>(language, body); 371 } 372 373 /** A class use to facilitate the processing of bits stream data. */ 374 private static final class BitStreamReader { 375 /** The bits stream represent by a bytes array. */ 376 private final byte[] mData; 377 378 /** The offset of the current byte. */ 379 private int mCurrentOffset; 380 381 /** 382 * The remained bits of the current byte which have not been read. The most significant 383 * will be read first, so the remained bits are always the least significant bits. 384 */ 385 private int mRemainedBit; 386 387 /** 388 * Constructor 389 * @param data bit stream data represent by byte array. 390 * @param offset the offset of the first byte. 391 */ BitStreamReader(byte[] data, int offset)392 BitStreamReader(byte[] data, int offset) { 393 mData = data; 394 mCurrentOffset = offset; 395 mRemainedBit = 8; 396 } 397 398 /** 399 * Read the first {@code count} bits. 400 * @param count the number of bits need to read 401 * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no 402 * greater than 32. 403 */ read(int count)404 public int read(int count) throws IndexOutOfBoundsException { 405 int val = 0; 406 while (count > 0) { 407 if (count >= mRemainedBit) { 408 val <<= mRemainedBit; 409 val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1); 410 count -= mRemainedBit; 411 mRemainedBit = 8; 412 ++mCurrentOffset; 413 } else { 414 val <<= count; 415 val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1)) 416 >> (mRemainedBit - count); 417 mRemainedBit -= count; 418 count = 0; 419 } 420 } 421 return val; 422 } 423 424 /** 425 * Skip the current bytes if the remained bits is less than 8. This is useful when 426 * processing the padding or reserved bits. 427 */ skip()428 public void skip() { 429 if (mRemainedBit < 8) { 430 mRemainedBit = 8; 431 ++mCurrentOffset; 432 } 433 } 434 } 435 } 436