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 android.annotation.NonNull; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.telephony.CbGeoUtils.Circle; 29 import android.telephony.CbGeoUtils.Geometry; 30 import android.telephony.CbGeoUtils.LatLng; 31 import android.telephony.CbGeoUtils.Polygon; 32 import android.telephony.SmsCbLocation; 33 import android.telephony.SmsCbMessage; 34 import android.telephony.SmsMessage; 35 import android.telephony.SubscriptionManager; 36 import android.util.Log; 37 import android.util.Pair; 38 39 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity; 40 import com.android.cellbroadcastservice.SmsCbHeader.DataCodingScheme; 41 import com.android.internal.annotations.VisibleForTesting; 42 43 import java.io.UnsupportedEncodingException; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.stream.Collectors; 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 */ 70 @VisibleForTesting getEtwsPrimaryMessage(Context context, int category)71 public static String getEtwsPrimaryMessage(Context context, int category) { 72 final Resources r = context.getResources(); 73 switch (category) { 74 case ETWS_WARNING_TYPE_EARTHQUAKE: 75 return r.getString(R.string.etws_primary_default_message_earthquake); 76 case ETWS_WARNING_TYPE_TSUNAMI: 77 return r.getString(R.string.etws_primary_default_message_tsunami); 78 case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI: 79 return r.getString(R.string.etws_primary_default_message_earthquake_and_tsunami); 80 case ETWS_WARNING_TYPE_TEST_MESSAGE: 81 return r.getString(R.string.etws_primary_default_message_test); 82 case ETWS_WARNING_TYPE_OTHER_EMERGENCY: 83 return r.getString(R.string.etws_primary_default_message_others); 84 default: 85 return ""; 86 } 87 } 88 89 /** 90 * Create a new SmsCbMessage object from a header object plus one or more received PDUs. 91 * 92 * @param pdus PDU bytes 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 SubscriptionManager sm = (SubscriptionManager) context.getSystemService( 98 Context.TELEPHONY_SUBSCRIPTION_SERVICE); 99 int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; 100 int[] subIds = sm.getSubscriptionIds(slotIndex); 101 if (subIds != null && subIds.length > 0) { 102 subId = subIds[0]; 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 Log.e(TAG, "create geo-fencing trigger failed, ex = " + ex.toString()); 210 return null; 211 } 212 } 213 214 /** 215 * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV. 216 * 217 * @param pdu Warning Area Coordinates TLV. 218 * @param wacOffset the offset of Warning Area Coordinates TLV. 219 * @return a pair with the first element is maximum wait time and the second is the broadcast 220 * area. The default value of the maximum wait time is 255 which means use the device default 221 * value. 222 */ parseWarningAreaCoordinates( byte[] pdu, int wacOffset)223 private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates( 224 byte[] pdu, int wacOffset) { 225 // little-endian 226 int wacDataLength = ((pdu[wacOffset + 1] & 0xff) << 8) | (pdu[wacOffset] & 0xff); 227 int offset = wacOffset + 2; 228 229 if (offset + wacDataLength > pdu.length) { 230 throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at" 231 + "least " + offset + wacDataLength + ", actual is " + pdu.length); 232 } 233 234 BitStreamReader bitReader = new BitStreamReader(pdu, offset); 235 236 int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET; 237 238 List<Geometry> geo = new ArrayList<>(); 239 int remainedBytes = wacDataLength; 240 while (remainedBytes > 0) { 241 int type = bitReader.read(4); 242 int length = bitReader.read(10); 243 remainedBytes -= length; 244 // Skip the 2 remained bits 245 bitReader.skip(); 246 247 switch (type) { 248 case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME: 249 maximumWaitTimeSec = bitReader.read(8); 250 break; 251 case CbGeoUtils.GEOMETRY_TYPE_POLYGON: 252 List<LatLng> latLngs = new ArrayList<>(); 253 // Each coordinate is represented by 44 bits integer. 254 // ATIS-0700041 5.2.4 Coordinate coding 255 int n = (length - 2) * 8 / 44; 256 for (int i = 0; i < n; i++) { 257 latLngs.add(getLatLng(bitReader)); 258 } 259 // Skip the padding bits 260 bitReader.skip(); 261 geo.add(new Polygon(latLngs)); 262 break; 263 case CbGeoUtils.GEOMETRY_TYPE_CIRCLE: 264 LatLng center = getLatLng(bitReader); 265 // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the 266 // distance unit during geo-fencing. 267 // ATIS-0700041 5.2.5 radius coding 268 double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0; 269 geo.add(new Circle(center, radius)); 270 break; 271 default: 272 throw new IllegalArgumentException("Unsupported geoType = " + type); 273 } 274 } 275 return new Pair(maximumWaitTimeSec, geo); 276 } 277 278 /** 279 * The coordinate is (latitude, longitude), represented by a 44 bits integer. 280 * The coding is defined in ATIS-0700041 5.2.4 281 * @param bitReader 282 * @return coordinate (latitude, longitude) 283 */ getLatLng(BitStreamReader bitReader)284 private static LatLng getLatLng(BitStreamReader bitReader) { 285 // wacLatitude = floor(((latitude + 90) / 180) * 2^22) 286 // wacLongitude = floor(((longitude + 180) / 360) * 2^22) 287 int wacLat = bitReader.read(22); 288 int wacLng = bitReader.read(22); 289 290 // latitude = wacLatitude * 180 / 2^22 - 90 291 // longitude = wacLongitude * 360 / 2^22 -180 292 return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180)); 293 } 294 295 /** 296 * Parse and unpack the UMTS body text according to the encoding in the data coding scheme. 297 * 298 * @param header the message header to use 299 * @param pdu the PDU to decode 300 * @return a pair of string containing the language and body of the message in order 301 */ parseUmtsBody(SmsCbHeader header, byte[] pdu)302 private static Pair<String, String> parseUmtsBody(SmsCbHeader header, 303 byte[] pdu) { 304 // Payload may contain multiple pages 305 int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; 306 String language = header.getDataCodingSchemeStructedData().language; 307 308 if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) 309 * nrPages) { 310 throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " 311 + nrPages + " pages"); 312 } 313 314 StringBuilder sb = new StringBuilder(); 315 316 for (int i = 0; i < nrPages; i++) { 317 // Each page is 82 bytes followed by a length octet indicating 318 // the number of useful octets within those 82 319 int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; 320 int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; 321 322 if (length > PDU_BODY_PAGE_LENGTH) { 323 throw new IllegalArgumentException("Page length " + length 324 + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); 325 } 326 327 Pair<String, String> p = unpackBody(pdu, offset, length, 328 header.getDataCodingSchemeStructedData()); 329 language = p.first; 330 sb.append(p.second); 331 } 332 return new Pair(language, sb.toString()); 333 334 } 335 336 /** 337 * Parse and unpack the GSM body text according to the encoding in the data coding scheme. 338 * @param header the message header to use 339 * @param pdu the PDU to decode 340 * @return a pair of string containing the language and body of the message in order 341 */ parseGsmBody(SmsCbHeader header, byte[] pdu)342 private static Pair<String, String> parseGsmBody(SmsCbHeader header, 343 byte[] pdu) { 344 // Payload is one single page 345 int offset = SmsCbHeader.PDU_HEADER_LENGTH; 346 int length = pdu.length - offset; 347 return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData()); 348 } 349 350 /** 351 * Unpack body text from the pdu using the given encoding, position and length within the pdu. 352 * 353 * @param pdu The pdu 354 * @param offset Position of the first byte to unpack 355 * @param length Number of bytes to unpack 356 * @param dcs data coding scheme 357 * @return a Pair of Strings containing the language and body of the message 358 */ unpackBody(byte[] pdu, int offset, int length, DataCodingScheme dcs)359 private static Pair<String, String> unpackBody(byte[] pdu, int offset, 360 int length, DataCodingScheme dcs) { 361 String body = null; 362 363 String language = dcs.language; 364 switch (dcs.encoding) { 365 case SmsMessage.ENCODING_7BIT: 366 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); 367 368 if (dcs.hasLanguageIndicator && body != null && body.length() > 2) { 369 // Language is two GSM characters followed by a CR. 370 // The actual body text is offset by 3 characters. 371 language = body.substring(0, 2); 372 body = body.substring(3); 373 } 374 break; 375 376 case SmsMessage.ENCODING_16BIT: 377 if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) { 378 // Language is two GSM characters. 379 // The actual body text is offset by 2 bytes. 380 language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); 381 offset += 2; 382 length -= 2; 383 } 384 385 try { 386 body = new String(pdu, offset, (length & 0xfffe), "utf-16"); 387 } catch (UnsupportedEncodingException e) { 388 // Apparently it wasn't valid UTF-16. 389 throw new IllegalArgumentException("Error decoding UTF-16 message", e); 390 } 391 break; 392 393 default: 394 break; 395 } 396 397 if (body != null) { 398 // Remove trailing carriage return 399 for (int i = body.length() - 1; i >= 0; i--) { 400 if (body.charAt(i) != CARRIAGE_RETURN) { 401 body = body.substring(0, i + 1); 402 break; 403 } 404 } 405 } else { 406 body = ""; 407 } 408 409 return new Pair<String, String>(language, body); 410 } 411 412 /** A class use to facilitate the processing of bits stream data. */ 413 private static final class BitStreamReader { 414 /** The bits stream represent by a bytes array. */ 415 private final byte[] mData; 416 417 /** The offset of the current byte. */ 418 private int mCurrentOffset; 419 420 /** 421 * The remained bits of the current byte which have not been read. The most significant 422 * will be read first, so the remained bits are always the least significant bits. 423 */ 424 private int mRemainedBit; 425 426 /** 427 * Constructor 428 * @param data bit stream data represent by byte array. 429 * @param offset the offset of the first byte. 430 */ BitStreamReader(byte[] data, int offset)431 BitStreamReader(byte[] data, int offset) { 432 mData = data; 433 mCurrentOffset = offset; 434 mRemainedBit = 8; 435 } 436 437 /** 438 * Read the first {@code count} bits. 439 * @param count the number of bits need to read 440 * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no 441 * greater than 32. 442 */ read(int count)443 public int read(int count) throws IndexOutOfBoundsException { 444 int val = 0; 445 while (count > 0) { 446 if (count >= mRemainedBit) { 447 val <<= mRemainedBit; 448 val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1); 449 count -= mRemainedBit; 450 mRemainedBit = 8; 451 ++mCurrentOffset; 452 } else { 453 val <<= count; 454 val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1)) 455 >> (mRemainedBit - count); 456 mRemainedBit -= count; 457 count = 0; 458 } 459 } 460 return val; 461 } 462 463 /** 464 * Skip the current bytes if the remained bits is less than 8. This is useful when 465 * processing the padding or reserved bits. 466 */ skip()467 public void skip() { 468 if (mRemainedBit < 8) { 469 mRemainedBit = 8; 470 ++mCurrentOffset; 471 } 472 } 473 } 474 475 /** 476 * Part of a GSM SMS cell broadcast message which may trigger geo-fencing logic. 477 * @hide 478 */ 479 public static final class GeoFencingTriggerMessage { 480 /** 481 * Indicate the list of active alerts share their warning area coordinates which means the 482 * broadcast area is the union of the broadcast areas of the active alerts in this list. 483 */ 484 public static final int TYPE_ACTIVE_ALERT_SHARE_WAC = 2; 485 486 public final int type; 487 public final List<CellBroadcastIdentity> cbIdentifiers; 488 GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers)489 GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers) { 490 this.type = type; 491 this.cbIdentifiers = cbIdentifiers; 492 } 493 494 /** 495 * Whether the trigger message indicates that the broadcast areas are shared between all 496 * active alerts. 497 * @return true if broadcast areas are to be shared 498 */ shouldShareBroadcastArea()499 boolean shouldShareBroadcastArea() { 500 return type == TYPE_ACTIVE_ALERT_SHARE_WAC; 501 } 502 503 /** 504 * The GSM cell broadcast identity 505 */ 506 @VisibleForTesting 507 public static final class CellBroadcastIdentity { 508 public final int messageIdentifier; 509 public final int serialNumber; CellBroadcastIdentity(int messageIdentifier, int serialNumber)510 CellBroadcastIdentity(int messageIdentifier, int serialNumber) { 511 this.messageIdentifier = messageIdentifier; 512 this.serialNumber = serialNumber; 513 } 514 } 515 516 @Override toString()517 public String toString() { 518 String identifiers = cbIdentifiers.stream() 519 .map(cbIdentifier ->String.format("(msgId = %d, serial = %d)", 520 cbIdentifier.messageIdentifier, cbIdentifier.serialNumber)) 521 .collect(Collectors.joining(",")); 522 return "triggerType=" + type + " identifiers=" + identifiers; 523 } 524 } 525 } 526