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.cellbroadcastservice; 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 static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_INVALID_GEO_FENCING_DATA; 26 import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_UMTS_INVALID_WAC; 27 28 import android.annotation.NonNull; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.telephony.CbGeoUtils.Circle; 32 import android.telephony.CbGeoUtils.Geometry; 33 import android.telephony.CbGeoUtils.LatLng; 34 import android.telephony.CbGeoUtils.Polygon; 35 import android.telephony.SmsCbLocation; 36 import android.telephony.SmsCbMessage; 37 import android.telephony.SmsMessage; 38 import android.telephony.SubscriptionManager; 39 import android.util.Log; 40 import android.util.Pair; 41 42 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity; 43 import com.android.cellbroadcastservice.SmsCbHeader.DataCodingScheme; 44 import com.android.internal.annotations.VisibleForTesting; 45 46 import java.io.UnsupportedEncodingException; 47 import java.util.ArrayList; 48 import java.util.List; 49 import java.util.stream.Collectors; 50 51 /** 52 * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is 53 * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases. 54 */ 55 public class GsmSmsCbMessage { 56 private static final String TAG = GsmSmsCbMessage.class.getSimpleName(); 57 58 private static final char CARRIAGE_RETURN = 0x0d; 59 60 private static final int PDU_BODY_PAGE_LENGTH = 82; 61 62 /** Utility class with only static methods. */ GsmSmsCbMessage()63 private GsmSmsCbMessage() { } 64 65 /** 66 * Get built-in ETWS primary messages by category. ETWS primary message does not contain text, 67 * so we have to show the pre-built messages to the user. 68 * 69 * @param context Device context 70 * @param category ETWS message category defined in SmsCbConstants 71 * @return ETWS text message in string. Return an empty string if no match. 72 */ 73 @VisibleForTesting getEtwsPrimaryMessage(Context context, int category)74 public 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 */ createSmsCbMessage(Context context, SmsCbHeader header, SmsCbLocation location, byte[][] pdus, int slotIndex)97 public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header, 98 SmsCbLocation location, byte[][] pdus, int slotIndex) 99 throws IllegalArgumentException { 100 SubscriptionManager sm = (SubscriptionManager) context.getSystemService( 101 Context.TELEPHONY_SUBSCRIPTION_SERVICE); 102 int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; 103 int[] subIds = sm.getSubscriptionIds(slotIndex); 104 if (subIds != null && subIds.length > 0) { 105 subId = subIds[0]; 106 } 107 108 long receivedTimeMillis = System.currentTimeMillis(); 109 if (header.isEtwsPrimaryNotification()) { 110 // ETSI TS 23.041 ETWS Primary Notification message 111 // ETWS primary message only contains 4 fields including serial number, 112 // message identifier, warning type, and warning security information. 113 // There is no field for the content/text so we get the text from the resources. 114 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(), 115 header.getSerialNumber(), location, header.getServiceCategory(), null, 116 header.getDataCodingScheme(), getEtwsPrimaryMessage(context, 117 header.getEtwsInfo().getWarningType()), SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, 118 header.getEtwsInfo(), header.getCmasInfo(), 0, null, receivedTimeMillis, 119 slotIndex, subId); 120 } else if (header.isUmtsFormat()) { 121 // UMTS format has only 1 PDU 122 byte[] pdu = pdus[0]; 123 Pair<String, String> cbData = parseUmtsBody(header, pdu); 124 String language = cbData.first; 125 String body = cbData.second; 126 int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY 127 : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; 128 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 129 int wacDataOffset = SmsCbHeader.PDU_HEADER_LENGTH 130 + 1 // number of pages 131 + (PDU_BODY_PAGE_LENGTH + 1) * nrPages; // cb data 132 133 // Has Warning Area Coordinates information 134 List<Geometry> geometries = null; 135 int maximumWaitingTimeSec = 255; 136 if (pdu.length > wacDataOffset) { 137 try { 138 Pair<Integer, List<Geometry>> wac = parseWarningAreaCoordinates(pdu, 139 wacDataOffset); 140 maximumWaitingTimeSec = wac.first; 141 geometries = wac.second; 142 } catch (Exception ex) { 143 // Catch the exception here, the message will be considered as having no WAC 144 // information which means the message will be broadcasted directly. 145 Log.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString()); 146 } 147 } 148 149 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 150 header.getGeographicalScope(), header.getSerialNumber(), location, 151 header.getServiceCategory(), language, header.getDataCodingScheme(), body, 152 priority, header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec, 153 geometries, receivedTimeMillis, slotIndex, subId); 154 } else { 155 String language = null; 156 StringBuilder sb = new StringBuilder(); 157 for (byte[] pdu : pdus) { 158 Pair<String, String> p = parseGsmBody(header, pdu); 159 language = p.first; 160 sb.append(p.second); 161 } 162 int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY 163 : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; 164 165 return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 166 header.getGeographicalScope(), header.getSerialNumber(), location, 167 header.getServiceCategory(), language, header.getDataCodingScheme(), 168 sb.toString(), priority, header.getEtwsInfo(), header.getCmasInfo(), 0, null, 169 receivedTimeMillis, slotIndex, subId); 170 } 171 } 172 173 /** 174 * Parse WEA Handset Action Message(WHAM) a.k.a geo-fencing trigger message. 175 * 176 * WEA Handset Action Message(WHAM) is a cell broadcast service message broadcast by the network 177 * to direct devices to perform a geo-fencing check on selected alerts. 178 * 179 * WEA Handset Action Message(WHAM) requirements from ATIS-0700041 section 4 180 * 1. The Warning Message contents of a WHAM shall be in Cell Broadcast(CB) data format as 181 * defined in TS 23.041. 182 * 2. The Warning Message Contents of WHAM shall be limited to one CB page(max 20 referenced 183 * WEA messages). 184 * 3. The broadcast area for a WHAM shall be the union of the broadcast areas of the referenced 185 * WEA message. 186 * @param pdu cell broadcast pdu, including the header 187 * @return {@link GeoFencingTriggerMessage} instance 188 */ createGeoFencingTriggerMessage(byte[] pdu)189 public static GeoFencingTriggerMessage createGeoFencingTriggerMessage(byte[] pdu) { 190 try { 191 // Header length + 1(number of page). ATIS-0700041 define the number of page of 192 // geo-fencing trigger message is 1. 193 int whamOffset = SmsCbHeader.PDU_HEADER_LENGTH + 1; 194 195 BitStreamReader bitReader = new BitStreamReader(pdu, whamOffset); 196 int type = bitReader.read(4); 197 int length = bitReader.read(7); 198 // Skip the remained 5 bits 199 bitReader.skip(); 200 201 int messageIdentifierCount = (length - 2) * 8 / 32; 202 List<CellBroadcastIdentity> cbIdentifiers = new ArrayList<>(); 203 for (int i = 0; i < messageIdentifierCount; i++) { 204 // Both messageIdentifier and serialNumber are 16 bits integers. 205 // ATIS-0700041 Section 5.1.6 206 int messageIdentifier = bitReader.read(16); 207 int serialNumber = bitReader.read(16); 208 cbIdentifiers.add(new CellBroadcastIdentity(messageIdentifier, serialNumber)); 209 } 210 return new GeoFencingTriggerMessage(type, cbIdentifiers); 211 } catch (Exception ex) { 212 final String errorMessage = "create geo-fencing trigger failed, ex = " + ex.toString(); 213 Log.e(TAG, errorMessage); 214 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR, 215 CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_INVALID_GEO_FENCING_DATA, 216 errorMessage); 217 return null; 218 } 219 } 220 221 /** 222 * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV. 223 * 224 * @param pdu Warning Area Coordinates TLV. 225 * @param wacOffset the offset of Warning Area Coordinates TLV. 226 * @return a pair with the first element is maximum wait time and the second is the broadcast 227 * area. The default value of the maximum wait time is 255 which means use the device default 228 * value. 229 */ parseWarningAreaCoordinates( byte[] pdu, int wacOffset)230 private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates( 231 byte[] pdu, int wacOffset) { 232 // little-endian 233 int wacDataLength = ((pdu[wacOffset + 1] & 0xff) << 8) | (pdu[wacOffset] & 0xff); 234 int offset = wacOffset + 2; 235 236 if (offset + wacDataLength > pdu.length) { 237 IllegalArgumentException ex = new IllegalArgumentException( 238 "Invalid wac data, expected the length of pdu at least " 239 + (offset + wacDataLength) + ", actual is " + pdu.length); 240 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR, 241 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_UMTS_INVALID_WAC, 242 ex.toString()); 243 throw ex; 244 } 245 246 BitStreamReader bitReader = new BitStreamReader(pdu, offset); 247 248 int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET; 249 250 List<Geometry> geo = new ArrayList<>(); 251 int remainedBytes = wacDataLength; 252 while (remainedBytes > 0) { 253 int type = bitReader.read(4); 254 int length = bitReader.read(10); 255 remainedBytes -= length; 256 // Skip the 2 remained bits 257 bitReader.skip(); 258 259 switch (type) { 260 case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME: 261 maximumWaitTimeSec = bitReader.read(8); 262 break; 263 case CbGeoUtils.GEOMETRY_TYPE_POLYGON: 264 List<LatLng> latLngs = new ArrayList<>(); 265 // Each coordinate is represented by 44 bits integer. 266 // ATIS-0700041 5.2.4 Coordinate coding 267 int n = (length - 2) * 8 / 44; 268 for (int i = 0; i < n; i++) { 269 latLngs.add(getLatLng(bitReader)); 270 } 271 // Skip the padding bits 272 bitReader.skip(); 273 geo.add(new Polygon(latLngs)); 274 break; 275 case CbGeoUtils.GEOMETRY_TYPE_CIRCLE: 276 LatLng center = getLatLng(bitReader); 277 // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the 278 // distance unit during geo-fencing. 279 // ATIS-0700041 5.2.5 radius coding 280 double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0; 281 geo.add(new Circle(center, radius)); 282 break; 283 default: 284 IllegalArgumentException ex = new IllegalArgumentException( 285 "Unsupported geoType = " + type); 286 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR, 287 CELL_BROADCAST_MESSAGE_ERROR__TYPE__GSM_UMTS_INVALID_WAC, 288 ex.toString()); 289 throw ex; 290 } 291 } 292 return new Pair(maximumWaitTimeSec, geo); 293 } 294 295 /** 296 * The coordinate is (latitude, longitude), represented by a 44 bits integer. 297 * The coding is defined in ATIS-0700041 5.2.4 298 * @param bitReader 299 * @return coordinate (latitude, longitude) 300 */ getLatLng(BitStreamReader bitReader)301 private static LatLng getLatLng(BitStreamReader bitReader) { 302 // wacLatitude = floor(((latitude + 90) / 180) * 2^22) 303 // wacLongitude = floor(((longitude + 180) / 360) * 2^22) 304 int wacLat = bitReader.read(22); 305 int wacLng = bitReader.read(22); 306 307 // latitude = wacLatitude * 180 / 2^22 - 90 308 // longitude = wacLongitude * 360 / 2^22 -180 309 return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180)); 310 } 311 312 /** 313 * Parse and unpack the UMTS body text according to the encoding in the data coding scheme. 314 * 315 * @param header the message header to use 316 * @param pdu the PDU to decode 317 * @return a pair of string containing the language and body of the message in order 318 */ parseUmtsBody(SmsCbHeader header, byte[] pdu)319 private static Pair<String, String> parseUmtsBody(SmsCbHeader header, 320 byte[] pdu) { 321 // Payload may contain multiple pages 322 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 323 String language = header.getDataCodingSchemeStructedData().language; 324 325 if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) 326 * nrPages) { 327 throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " 328 + nrPages + " pages"); 329 } 330 331 StringBuilder sb = new StringBuilder(); 332 333 for (int i = 0; i < nrPages; i++) { 334 // Each page is 82 bytes followed by a length octet indicating 335 // the number of useful octets within those 82 336 int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; 337 int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; 338 339 if (length > PDU_BODY_PAGE_LENGTH) { 340 throw new IllegalArgumentException("Page length " + length 341 + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); 342 } 343 344 Pair<String, String> p = unpackBody(pdu, offset, length, 345 header.getDataCodingSchemeStructedData()); 346 language = p.first; 347 sb.append(p.second); 348 } 349 return new Pair(language, sb.toString()); 350 351 } 352 353 /** 354 * Parse and unpack the GSM body text according to the encoding in the data coding scheme. 355 * @param header the message header to use 356 * @param pdu the PDU to decode 357 * @return a pair of string containing the language and body of the message in order 358 */ parseGsmBody(SmsCbHeader header, byte[] pdu)359 private static Pair<String, String> parseGsmBody(SmsCbHeader header, 360 byte[] pdu) { 361 // Payload is one single page 362 int offset = SmsCbHeader.PDU_HEADER_LENGTH; 363 int length = pdu.length - offset; 364 return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData()); 365 } 366 367 /** 368 * Unpack body text from the pdu using the given encoding, position and length within the pdu. 369 * 370 * @param pdu The pdu 371 * @param offset Position of the first byte to unpack 372 * @param length Number of bytes to unpack 373 * @param dcs data coding scheme 374 * @return a Pair of Strings containing the language and body of the message 375 */ unpackBody(byte[] pdu, int offset, int length, DataCodingScheme dcs)376 private static Pair<String, String> unpackBody(byte[] pdu, int offset, 377 int length, DataCodingScheme dcs) { 378 String body = null; 379 380 String language = dcs.language; 381 switch (dcs.encoding) { 382 case SmsMessage.ENCODING_7BIT: 383 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); 384 385 if (dcs.hasLanguageIndicator && body != null && body.length() > 2) { 386 // Language is two GSM characters followed by a CR. 387 // The actual body text is offset by 3 characters. 388 language = body.substring(0, 2); 389 body = body.substring(3); 390 } 391 break; 392 393 case SmsMessage.ENCODING_8BIT: 394 // Support decoding the pdu as pack GSM 8-bit (a GSM alphabet string that's stored 395 // in 8-bit unpacked format) characters. 396 body = GsmAlphabet.gsm8BitUnpackedToString(pdu, offset, length); 397 break; 398 399 case SmsMessage.ENCODING_16BIT: 400 if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) { 401 // Language is two GSM characters. 402 // The actual body text is offset by 2 bytes. 403 language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); 404 offset += 2; 405 length -= 2; 406 } 407 408 try { 409 body = new String(pdu, offset, (length & 0xfffe), "utf-16"); 410 } catch (UnsupportedEncodingException e) { 411 // Apparently it wasn't valid UTF-16. 412 throw new IllegalArgumentException("Error decoding UTF-16 message", e); 413 } 414 break; 415 416 default: 417 break; 418 } 419 420 if (body != null) { 421 // Remove trailing carriage return 422 for (int i = body.length() - 1; i >= 0; i--) { 423 if (body.charAt(i) != CARRIAGE_RETURN) { 424 body = body.substring(0, i + 1); 425 break; 426 } 427 } 428 } else { 429 body = ""; 430 } 431 432 return new Pair<String, String>(language, body); 433 } 434 435 /** A class use to facilitate the processing of bits stream data. */ 436 private static final class BitStreamReader { 437 /** The bits stream represent by a bytes array. */ 438 private final byte[] mData; 439 440 /** The offset of the current byte. */ 441 private int mCurrentOffset; 442 443 /** 444 * The remained bits of the current byte which have not been read. The most significant 445 * will be read first, so the remained bits are always the least significant bits. 446 */ 447 private int mRemainedBit; 448 449 /** 450 * Constructor 451 * @param data bit stream data represent by byte array. 452 * @param offset the offset of the first byte. 453 */ BitStreamReader(byte[] data, int offset)454 BitStreamReader(byte[] data, int offset) { 455 mData = data; 456 mCurrentOffset = offset; 457 mRemainedBit = 8; 458 } 459 460 /** 461 * Read the first {@code count} bits. 462 * @param count the number of bits need to read 463 * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no 464 * greater than 32. 465 */ read(int count)466 public int read(int count) throws IndexOutOfBoundsException { 467 int val = 0; 468 while (count > 0) { 469 if (count >= mRemainedBit) { 470 val <<= mRemainedBit; 471 val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1); 472 count -= mRemainedBit; 473 mRemainedBit = 8; 474 ++mCurrentOffset; 475 } else { 476 val <<= count; 477 val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1)) 478 >> (mRemainedBit - count); 479 mRemainedBit -= count; 480 count = 0; 481 } 482 } 483 return val; 484 } 485 486 /** 487 * Skip the current bytes if the remained bits is less than 8. This is useful when 488 * processing the padding or reserved bits. 489 */ skip()490 public void skip() { 491 if (mRemainedBit < 8) { 492 mRemainedBit = 8; 493 ++mCurrentOffset; 494 } 495 } 496 } 497 498 /** 499 * Part of a GSM SMS cell broadcast message which may trigger geo-fencing logic. 500 * @hide 501 */ 502 public static final class GeoFencingTriggerMessage { 503 /** 504 * Indicate the list of active alerts share their warning area coordinates which means the 505 * broadcast area is the union of the broadcast areas of the active alerts in this list. 506 */ 507 public static final int TYPE_ACTIVE_ALERT_SHARE_WAC = 2; 508 509 public final int type; 510 public final List<CellBroadcastIdentity> cbIdentifiers; 511 GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers)512 GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers) { 513 this.type = type; 514 this.cbIdentifiers = cbIdentifiers; 515 } 516 517 /** 518 * Whether the trigger message indicates that the broadcast areas are shared between all 519 * active alerts. 520 * @return true if broadcast areas are to be shared 521 */ shouldShareBroadcastArea()522 boolean shouldShareBroadcastArea() { 523 return type == TYPE_ACTIVE_ALERT_SHARE_WAC; 524 } 525 526 /** 527 * The GSM cell broadcast identity 528 */ 529 @VisibleForTesting 530 public static final class CellBroadcastIdentity { 531 public final int messageIdentifier; 532 public final int serialNumber; CellBroadcastIdentity(int messageIdentifier, int serialNumber)533 CellBroadcastIdentity(int messageIdentifier, int serialNumber) { 534 this.messageIdentifier = messageIdentifier; 535 this.serialNumber = serialNumber; 536 } 537 } 538 539 @Override toString()540 public String toString() { 541 String identifiers = cbIdentifiers.stream() 542 .map(cbIdentifier ->String.format("(msgId = %d, serial = %d)", 543 cbIdentifier.messageIdentifier, cbIdentifier.serialNumber)) 544 .collect(Collectors.joining(",")); 545 return "triggerType=" + type + " identifiers=" + identifiers; 546 } 547 } 548 } 549