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