1 /* 2 * Copyright (C) 2013 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 android.annotation.NonNull; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.Looper; 28 import android.os.Message; 29 import android.os.SystemClock; 30 import android.os.UserHandle; 31 import android.provider.Telephony.CellBroadcasts; 32 import android.telephony.CbGeoUtils.Geometry; 33 import android.telephony.CellBroadcastIntents; 34 import android.telephony.CellIdentity; 35 import android.telephony.CellIdentityGsm; 36 import android.telephony.CellInfo; 37 import android.telephony.SmsCbLocation; 38 import android.telephony.SmsCbMessage; 39 import android.telephony.SubscriptionManager; 40 import android.telephony.TelephonyManager; 41 import android.text.TextUtils; 42 import android.text.format.DateUtils; 43 import android.util.Pair; 44 import android.util.SparseArray; 45 46 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage; 47 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity; 48 import com.android.internal.annotations.VisibleForTesting; 49 50 import java.text.DateFormat; 51 import java.util.ArrayList; 52 import java.util.HashMap; 53 import java.util.Iterator; 54 import java.util.List; 55 import java.util.stream.IntStream; 56 57 /** 58 * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts. 59 */ 60 public class GsmCellBroadcastHandler extends CellBroadcastHandler { 61 private static final boolean VDBG = false; // log CB PDU data 62 63 /** Indicates that a message is not displayed. */ 64 private static final String MESSAGE_NOT_DISPLAYED = "0"; 65 66 private final SparseArray<String> mAreaInfos = new SparseArray<>(); 67 68 /** This map holds incomplete concatenated messages waiting for assembly. */ 69 private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = 70 new HashMap<>(4); 71 72 @VisibleForTesting GsmCellBroadcastHandler(Context context, Looper looper)73 public GsmCellBroadcastHandler(Context context, Looper looper) { 74 super("GsmCellBroadcastHandler", context, looper); 75 } 76 77 @Override onQuitting()78 protected void onQuitting() { 79 super.onQuitting(); // release wakelock 80 } 81 82 /** 83 * Handle a GSM cell broadcast message passed from the telephony framework. 84 * @param message 85 */ onGsmCellBroadcastSms(int slotIndex, byte[] message)86 public void onGsmCellBroadcastSms(int slotIndex, byte[] message) { 87 sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message); 88 } 89 90 /** 91 * Get the area information 92 * 93 * @param slotIndex SIM slot index 94 * @return The area information 95 */ 96 @NonNull getCellBroadcastAreaInfo(int slotIndex)97 public String getCellBroadcastAreaInfo(int slotIndex) { 98 String info; 99 synchronized (mAreaInfos) { 100 info = mAreaInfos.get(slotIndex); 101 } 102 return info == null ? "" : info; 103 } 104 105 /** 106 * Create a new CellBroadcastHandler. 107 * @param context the context to use for dispatching Intents 108 * @return the new handler 109 */ makeGsmCellBroadcastHandler(Context context)110 public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) { 111 GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context, Looper.myLooper()); 112 handler.start(); 113 return handler; 114 } 115 116 /** 117 * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a 118 * geo-fencing check for these messages. 119 * @param geoFencingTriggerMessage the trigger message 120 * 121 * @return {@code True} if geo-fencing is need for some cell broadcast message. 122 */ handleGeoFencingTriggerMessage( GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex)123 private boolean handleGeoFencingTriggerMessage( 124 GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) { 125 final List<SmsCbMessage> cbMessages = new ArrayList<>(); 126 final List<Uri> cbMessageUris = new ArrayList<>(); 127 128 SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService( 129 Context.TELEPHONY_SUBSCRIPTION_SERVICE); 130 int[] subIds = subMgr.getSubscriptionIds(slotIndex); 131 Resources res; 132 if (subIds != null) { 133 res = getResources(subIds[0]); 134 } else { 135 res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); 136 } 137 138 // Only consider the cell broadcast received within 24 hours. 139 long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS; 140 141 // Some carriers require reset duplication detection after airplane mode or reboot. 142 if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) { 143 lastReceivedTime = Long.max(lastReceivedTime, mLastAirplaneModeTime); 144 lastReceivedTime = Long.max(lastReceivedTime, 145 System.currentTimeMillis() - SystemClock.elapsedRealtime()); 146 } 147 148 // Find the cell broadcast message identify by the message identifier and serial number 149 // and was not displayed. 150 String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND " 151 + CellBroadcasts.SERIAL_NUMBER + "=? AND " 152 + CellBroadcasts.MESSAGE_DISPLAYED + "=? AND " 153 + CellBroadcasts.RECEIVED_TIME + ">?"; 154 155 ContentResolver resolver = mContext.getContentResolver(); 156 for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) { 157 try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI, 158 CellBroadcastProvider.QUERY_COLUMNS, 159 where, 160 new String[] { Integer.toString(identity.messageIdentifier), 161 Integer.toString(identity.serialNumber), MESSAGE_NOT_DISPLAYED, 162 Long.toString(lastReceivedTime) }, 163 null /* sortOrder */)) { 164 if (cursor != null) { 165 while (cursor.moveToNext()) { 166 cbMessages.add(SmsCbMessage.createFromCursor(cursor)); 167 cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI, 168 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID)))); 169 } 170 } 171 } 172 } 173 174 log("Found " + cbMessages.size() + " not broadcasted messages since " 175 + DateFormat.getDateTimeInstance().format(lastReceivedTime)); 176 177 List<Geometry> commonBroadcastArea = new ArrayList<>(); 178 if (geoFencingTriggerMessage.shouldShareBroadcastArea()) { 179 for (SmsCbMessage msg : cbMessages) { 180 if (msg.getGeometries() != null) { 181 commonBroadcastArea.addAll(msg.getGeometries()); 182 } 183 } 184 } 185 186 // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified 187 // in geo fencing trigger message. We will pick the largest maximum wait time among these 188 // cell broadcasts. 189 int maximumWaitTimeSec = 0; 190 for (SmsCbMessage msg : cbMessages) { 191 maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingDuration()); 192 } 193 194 if (DBG) { 195 logd("Geo-fencing trigger message = " + geoFencingTriggerMessage); 196 for (SmsCbMessage msg : cbMessages) { 197 logd(msg.toString()); 198 } 199 } 200 201 if (cbMessages.isEmpty()) { 202 if (DBG) logd("No CellBroadcast message need to be broadcasted"); 203 return false; 204 } 205 206 requestLocationUpdate(location -> { 207 if (location == null) { 208 // If the location is not available, broadcast the messages directly. 209 for (int i = 0; i < cbMessages.size(); i++) { 210 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex); 211 } 212 } else { 213 for (int i = 0; i < cbMessages.size(); i++) { 214 List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty() 215 ? commonBroadcastArea : cbMessages.get(i).getGeometries(); 216 if (broadcastArea == null || broadcastArea.isEmpty()) { 217 broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex); 218 } else { 219 performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea, 220 location, slotIndex); 221 } 222 } 223 } 224 }, maximumWaitTimeSec); 225 return true; 226 } 227 228 /** 229 * Process area info message. 230 * 231 * @param slotIndex SIM slot index 232 * @param message Cell broadcast message 233 * @return {@code true} if the mssage is an area info message and got processed correctly, 234 * otherwise {@code false}. 235 */ handleAreaInfoMessage(int slotIndex, SmsCbMessage message)236 private boolean handleAreaInfoMessage(int slotIndex, SmsCbMessage message) { 237 SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService( 238 Context.TELEPHONY_SUBSCRIPTION_SERVICE); 239 240 // Check area info message 241 int[] subIds = subMgr.getSubscriptionIds(slotIndex); 242 Resources res; 243 if (subIds != null) { 244 res = getResources(subIds[0]); 245 } else { 246 res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); 247 } 248 int[] areaInfoChannels = res.getIntArray(R.array.area_info_channels); 249 250 if (IntStream.of(areaInfoChannels).anyMatch( 251 x -> x == message.getServiceCategory())) { 252 synchronized (mAreaInfos) { 253 String info = mAreaInfos.get(slotIndex); 254 if (TextUtils.equals(info, message.getMessageBody())) { 255 // Message is a duplicate 256 return true; 257 } 258 mAreaInfos.put(slotIndex, message.getMessageBody()); 259 } 260 261 String[] pkgs = mContext.getResources().getStringArray( 262 R.array.config_area_info_receiver_packages); 263 for (String pkg : pkgs) { 264 Intent intent = new Intent(CellBroadcastIntents.ACTION_AREA_INFO_UPDATED); 265 intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, slotIndex); 266 intent.setPackage(pkg); 267 mContext.sendBroadcastAsUser(intent, UserHandle.ALL, 268 android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE); 269 } 270 return true; 271 } 272 273 // This is not an area info message. 274 return false; 275 } 276 277 /** 278 * Handle 3GPP-format Cell Broadcast messages sent from radio. 279 * 280 * @param message the message to process 281 * @return true if need to wait for geo-fencing or an ordered broadcast was sent. 282 */ 283 @Override handleSmsMessage(Message message)284 protected boolean handleSmsMessage(Message message) { 285 // For GSM, message.obj should be a byte[] 286 int slotIndex = message.arg1; 287 if (message.obj instanceof byte[]) { 288 byte[] pdu = (byte[]) message.obj; 289 SmsCbHeader header = createSmsCbHeader(pdu); 290 if (header == null) return false; 291 292 log("header=" + header); 293 if (header.getServiceCategory() == SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) { 294 GeoFencingTriggerMessage triggerMessage = 295 GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu); 296 if (triggerMessage != null) { 297 return handleGeoFencingTriggerMessage(triggerMessage, slotIndex); 298 } 299 } else { 300 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex); 301 if (cbMessage != null) { 302 if (isDuplicate(cbMessage)) { 303 return false; 304 } 305 306 if (handleAreaInfoMessage(slotIndex, cbMessage)) { 307 log("Channel " + cbMessage.getServiceCategory() + " message processed"); 308 return false; 309 } 310 311 handleBroadcastSms(cbMessage); 312 return true; 313 } 314 if (VDBG) log("Not handled GSM broadcasts."); 315 } 316 } 317 return super.handleSmsMessage(message); 318 } 319 320 // return the GSM cell location from the first GSM cell info getGsmLacAndCid()321 private Pair<Integer, Integer> getGsmLacAndCid() { 322 TelephonyManager tm = 323 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); 324 List<CellInfo> infos = tm.getAllCellInfo(); 325 for (CellInfo info : infos) { 326 CellIdentity ci = info.getCellIdentity(); 327 if (ci instanceof CellIdentityGsm) { 328 CellIdentityGsm ciGsm = (CellIdentityGsm) ci; 329 int lac = ciGsm.getLac() != CellInfo.UNAVAILABLE ? ciGsm.getLac() : -1; 330 int cid = ciGsm.getCid() != CellInfo.UNAVAILABLE ? ciGsm.getCid() : -1; 331 return Pair.create(lac, cid); 332 } 333 } 334 return null; 335 } 336 337 338 /** 339 * Handle 3GPP format SMS-CB message. 340 * @param header the cellbroadcast header. 341 * @param receivedPdu the received PDUs as a byte[] 342 */ handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu, int slotIndex)343 private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu, 344 int slotIndex) { 345 try { 346 if (VDBG) { 347 int pduLength = receivedPdu.length; 348 for (int i = 0; i < pduLength; i += 8) { 349 StringBuilder sb = new StringBuilder("SMS CB pdu data: "); 350 for (int j = i; j < i + 8 && j < pduLength; j++) { 351 int b = receivedPdu[j] & 0xff; 352 if (b < 0x10) { 353 sb.append('0'); 354 } 355 sb.append(Integer.toHexString(b)).append(' '); 356 } 357 log(sb.toString()); 358 } 359 } 360 361 if (VDBG) log("header=" + header); 362 TelephonyManager tm = 363 (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); 364 tm.createForSubscriptionId(getSubIdForPhone(mContext, slotIndex)); 365 // TODO make a systemAPI for getNetworkOperatorForSlotIndex 366 String plmn = tm.getSimOperator(); 367 int lac = -1; 368 int cid = -1; 369 Pair<Integer, Integer> lacAndCid = getGsmLacAndCid(); 370 // Check if GSM lac and cid are available. This is required to support 371 // dual-mode devices such as CDMA/LTE devices that require support for 372 // both 3GPP and 3GPP2 format messages 373 if (lacAndCid != null) { 374 lac = lacAndCid.first; 375 cid = lacAndCid.second; 376 } 377 378 SmsCbLocation location; 379 switch (header.getGeographicalScope()) { 380 case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE: 381 location = new SmsCbLocation(plmn, lac, -1); 382 break; 383 384 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: 385 case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: 386 location = new SmsCbLocation(plmn, lac, cid); 387 break; 388 389 case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: 390 default: 391 location = new SmsCbLocation(plmn, -1, -1); 392 break; 393 } 394 395 byte[][] pdus; 396 int pageCount = header.getNumberOfPages(); 397 if (pageCount > 1) { 398 // Multi-page message 399 SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location); 400 401 // Try to find other pages of the same message 402 pdus = mSmsCbPageMap.get(concatInfo); 403 404 if (pdus == null) { 405 // This is the first page of this message, make room for all 406 // pages and keep until complete 407 pdus = new byte[pageCount][]; 408 409 mSmsCbPageMap.put(concatInfo, pdus); 410 } 411 412 if (VDBG) log("pdus size=" + pdus.length); 413 // Page parameter is one-based 414 pdus[header.getPageIndex() - 1] = receivedPdu; 415 416 for (byte[] pdu : pdus) { 417 if (pdu == null) { 418 // Still missing pages, exit 419 log("still missing pdu"); 420 return null; 421 } 422 } 423 424 // Message complete, remove and dispatch 425 mSmsCbPageMap.remove(concatInfo); 426 } else { 427 // Single page message 428 pdus = new byte[1][]; 429 pdus[0] = receivedPdu; 430 } 431 432 // Remove messages that are out of scope to prevent the map from 433 // growing indefinitely, containing incomplete messages that were 434 // never assembled 435 Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator(); 436 437 while (iter.hasNext()) { 438 SmsCbConcatInfo info = iter.next(); 439 440 if (!info.matchesLocation(plmn, lac, cid)) { 441 iter.remove(); 442 } 443 } 444 445 return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus, slotIndex); 446 447 } catch (RuntimeException e) { 448 loge("Error in decoding SMS CB pdu", e); 449 return null; 450 } 451 } 452 createSmsCbHeader(byte[] bytes)453 private SmsCbHeader createSmsCbHeader(byte[] bytes) { 454 try { 455 return new SmsCbHeader(bytes); 456 } catch (Exception ex) { 457 loge("Can't create SmsCbHeader, ex = " + ex.toString()); 458 return null; 459 } 460 } 461 462 /** 463 * Holds all info about a message page needed to assemble a complete concatenated message. 464 */ 465 @VisibleForTesting 466 public static final class SmsCbConcatInfo { 467 468 private final SmsCbHeader mHeader; 469 private final SmsCbLocation mLocation; 470 471 @VisibleForTesting SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location)472 public SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) { 473 mHeader = header; 474 mLocation = location; 475 } 476 477 @Override hashCode()478 public int hashCode() { 479 return (mHeader.getSerialNumber() * 31) + mLocation.hashCode(); 480 } 481 482 @Override equals(Object obj)483 public boolean equals(Object obj) { 484 if (obj instanceof SmsCbConcatInfo) { 485 SmsCbConcatInfo other = (SmsCbConcatInfo) obj; 486 487 // Two pages match if they have the same serial number (which includes the 488 // geographical scope and update number), and both pages belong to the same 489 // location (PLMN, plus LAC and CID if these are part of the geographical scope). 490 return mHeader.getSerialNumber() == other.mHeader.getSerialNumber() 491 && mLocation.equals(other.mLocation); 492 } 493 494 return false; 495 } 496 497 /** 498 * Compare the location code for this message to the current location code. The match is 499 * relative to the geographical scope of the message, which determines whether the LAC 500 * and Cell ID are saved in mLocation or set to -1 to match all values. 501 * 502 * @param plmn the current PLMN 503 * @param lac the current Location Area (GSM) or Service Area (UMTS) 504 * @param cid the current Cell ID 505 * @return true if this message is valid for the current location; false otherwise 506 */ matchesLocation(String plmn, int lac, int cid)507 public boolean matchesLocation(String plmn, int lac, int cid) { 508 return mLocation.isInLocationArea(plmn, lac, cid); 509 } 510 } 511 } 512