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 static android.Manifest.permission.ACCESS_COARSE_LOCATION; 20 import static android.Manifest.permission.ACCESS_FINE_LOCATION; 21 22 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_AMBIGUOUS; 23 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_DONT_SEND; 24 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_NO_COORDINATES; 25 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_SEND; 26 import static com.android.cellbroadcastservice.CbSendMessageCalculator.SEND_MESSAGE_ACTION_SENT; 27 import static com.android.cellbroadcastservice.CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_CDMA_MESSAGE_TYPE_FROM_FWK; 28 29 import android.annotation.NonNull; 30 import android.annotation.Nullable; 31 import android.app.Activity; 32 import android.content.BroadcastReceiver; 33 import android.content.ContentValues; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.content.IntentFilter; 37 import android.content.pm.ApplicationInfo; 38 import android.content.pm.PackageManager; 39 import android.content.pm.ResolveInfo; 40 import android.content.res.Resources; 41 import android.database.Cursor; 42 import android.location.Location; 43 import android.location.LocationListener; 44 import android.location.LocationManager; 45 import android.location.LocationRequest; 46 import android.net.Uri; 47 import android.os.Bundle; 48 import android.os.Handler; 49 import android.os.Looper; 50 import android.os.Message; 51 import android.os.Process; 52 import android.os.SystemClock; 53 import android.os.SystemProperties; 54 import android.os.UserHandle; 55 import android.provider.Telephony; 56 import android.provider.Telephony.CellBroadcasts; 57 import android.telephony.CbGeoUtils.LatLng; 58 import android.telephony.CellBroadcastIntents; 59 import android.telephony.SmsCbMessage; 60 import android.telephony.SubscriptionManager; 61 import android.telephony.cdma.CdmaSmsCbProgramData; 62 import android.text.TextUtils; 63 import android.util.LocalLog; 64 import android.util.Log; 65 66 import com.android.internal.annotations.VisibleForTesting; 67 import com.android.modules.utils.HandlerExecutor; 68 69 import java.io.File; 70 import java.io.FileDescriptor; 71 import java.io.PrintWriter; 72 import java.text.DateFormat; 73 import java.util.ArrayList; 74 import java.util.Arrays; 75 import java.util.HashMap; 76 import java.util.List; 77 import java.util.Map; 78 import java.util.Objects; 79 import java.util.concurrent.TimeUnit; 80 import java.util.stream.Collectors; 81 import java.util.stream.Stream; 82 83 /** 84 * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast 85 * completes and our result receiver is called. 86 */ 87 public class CellBroadcastHandler extends WakeLockStateMachine { 88 private static final String TAG = "CellBroadcastHandler"; 89 90 /** 91 * CellBroadcast apex name 92 */ 93 private static final String CB_APEX_NAME = "com.android.cellbroadcast"; 94 95 /** 96 * CellBroadcast app platform name 97 */ 98 private static final String CB_APP_PLATFORM_NAME = "CellBroadcastAppPlatform"; 99 100 /** 101 * Path where CB apex is mounted (/apex/com.android.cellbroadcast) 102 */ 103 private static final String CB_APEX_PATH = new File("/apex", CB_APEX_NAME).getAbsolutePath(); 104 105 private static final String EXTRA_MESSAGE = "message"; 106 107 /** 108 * To disable cell broadcast duplicate detection for debugging purposes 109 * <code>adb shell am broadcast -a com.android.cellbroadcastservice.action.DUPLICATE_DETECTION 110 * --ez enable false</code> 111 * 112 * To enable cell broadcast duplicate detection for debugging purposes 113 * <code>adb shell am broadcast -a com.android.cellbroadcastservice.action.DUPLICATE_DETECTION 114 * --ez enable true</code> 115 */ 116 private static final String ACTION_DUPLICATE_DETECTION = 117 "com.android.cellbroadcastservice.action.DUPLICATE_DETECTION"; 118 119 /** 120 * The extra for cell broadcast duplicate detection enable/disable 121 */ 122 private static final String EXTRA_ENABLE = "enable"; 123 124 private final LocalLog mLocalLog = new LocalLog(100); 125 126 private static final boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1; 127 128 /** Uses to request the location update. */ 129 private final LocationRequester mLocationRequester; 130 131 /** Used to inject new calculators during unit testing */ 132 @NonNull protected final CbSendMessageCalculatorFactory mCbSendMessageCalculatorFactory; 133 134 /** Timestamp of last airplane mode on */ 135 protected long mLastAirplaneModeTime = 0; 136 137 /** Resource cache used for test purpose, to be removed by b/223644462 */ 138 protected final Map<Integer, Resources> mResourcesCache = new HashMap<>(); 139 140 /** Whether performing duplicate detection or not. Note this is for debugging purposes only. */ 141 private boolean mEnableDuplicateDetection = true; 142 143 /** 144 * Service category equivalent map. The key is the GSM service category, the value is the CDMA 145 * service category. 146 */ 147 private final Map<Integer, Integer> mServiceCategoryCrossRATMap; 148 149 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 150 @Override 151 public void onReceive(Context context, Intent intent) { 152 switch (intent.getAction()) { 153 case Intent.ACTION_AIRPLANE_MODE_CHANGED: 154 boolean airplaneModeOn = intent.getBooleanExtra("state", false); 155 if (airplaneModeOn) { 156 mLastAirplaneModeTime = System.currentTimeMillis(); 157 log("Airplane mode on."); 158 } 159 break; 160 case ACTION_DUPLICATE_DETECTION: 161 mEnableDuplicateDetection = intent.getBooleanExtra(EXTRA_ENABLE, 162 true); 163 log("Duplicate detection " + (mEnableDuplicateDetection 164 ? "enabled" : "disabled")); 165 break; 166 default: 167 log("Unhandled broadcast " + intent.getAction()); 168 } 169 } 170 }; 171 CellBroadcastHandler(Context context)172 private CellBroadcastHandler(Context context) { 173 this(CellBroadcastHandler.class.getSimpleName(), context, Looper.myLooper(), 174 new CbSendMessageCalculatorFactory(), null); 175 } 176 177 /** 178 * Allows tests to inject new calculators 179 */ 180 @VisibleForTesting 181 public static class CbSendMessageCalculatorFactory { CbSendMessageCalculatorFactory()182 public CbSendMessageCalculatorFactory() { 183 } 184 185 /** 186 * Creates new calculator 187 * @param context context 188 * @param fences the geo fences to use in the calculator 189 * @return a new instance of the calculator 190 */ createNew(@onNull final Context context, @NonNull final List<android.telephony.CbGeoUtils.Geometry> fences)191 public CbSendMessageCalculator createNew(@NonNull final Context context, 192 @NonNull final List<android.telephony.CbGeoUtils.Geometry> fences) { 193 return new CbSendMessageCalculator(context, fences); 194 } 195 } 196 197 @VisibleForTesting CellBroadcastHandler(String debugTag, Context context, Looper looper, @NonNull final CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory, @Nullable HandlerHelper handlerHelper)198 public CellBroadcastHandler(String debugTag, Context context, Looper looper, 199 @NonNull final CbSendMessageCalculatorFactory cbSendMessageCalculatorFactory, 200 @Nullable HandlerHelper handlerHelper) { 201 super(debugTag, context, looper); 202 203 if (handlerHelper == null) { 204 // Would have preferred to not have handlerHelper has nullable and pass this through the 205 // default ctor. Had trouble doing this because getHander() can't be called until 206 // the type is fully constructed. 207 handlerHelper = new HandlerHelper(getHandler()); 208 } 209 mCbSendMessageCalculatorFactory = cbSendMessageCalculatorFactory; 210 mLocationRequester = new LocationRequester( 211 context, 212 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE), 213 handlerHelper, getName()); 214 215 // Adding GSM / CDMA service category mapping. 216 mServiceCategoryCrossRATMap = Stream.of(new Integer[][] { 217 // Presidential alert 218 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL, 219 CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT}, 220 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE, 221 CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT}, 222 223 // Extreme alert 224 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED, 225 CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT}, 226 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE, 227 CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT}, 228 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY, 229 CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT}, 230 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE, 231 CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT}, 232 233 // Severe alert 234 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED, 235 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 236 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE, 237 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 238 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY, 239 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 240 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE, 241 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 242 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED, 243 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 244 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE, 245 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 246 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY, 247 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 248 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE, 249 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 250 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED, 251 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 252 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE, 253 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 254 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY, 255 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 256 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE, 257 CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT}, 258 259 // Amber alert 260 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY, 261 CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY}, 262 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE, 263 CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY}, 264 265 // Monthly test alert 266 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST, 267 CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE}, 268 { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE, 269 CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE}, 270 }).collect(Collectors.toMap(data -> data[0], data -> data[1])); 271 272 IntentFilter intentFilter = new IntentFilter(); 273 intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); 274 if (IS_DEBUGGABLE) { 275 intentFilter.addAction(ACTION_DUPLICATE_DETECTION); 276 } 277 278 mContext.registerReceiver(mReceiver, intentFilter, Context.RECEIVER_EXPORTED); 279 } 280 cleanup()281 public void cleanup() { 282 if (DBG) log("CellBroadcastHandler cleanup"); 283 mContext.unregisterReceiver(mReceiver); 284 } 285 286 /** 287 * Create a new CellBroadcastHandler. 288 * @param context the context to use for dispatching Intents 289 * @return the new handler 290 */ makeCellBroadcastHandler(Context context)291 public static CellBroadcastHandler makeCellBroadcastHandler(Context context) { 292 CellBroadcastHandler handler = new CellBroadcastHandler(context); 293 handler.start(); 294 return handler; 295 } 296 297 /** 298 * Handle Cell Broadcast messages from {@code CdmaInboundSmsHandler}. 299 * 3GPP-format Cell Broadcast messages sent from radio are handled in the subclass. 300 * 301 * @param message the message to process 302 * @return true if need to wait for geo-fencing or an ordered broadcast was sent. 303 */ 304 @Override handleSmsMessage(Message message)305 protected boolean handleSmsMessage(Message message) { 306 if (message.obj instanceof SmsCbMessage) { 307 if (!isDuplicate((SmsCbMessage) message.obj)) { 308 handleBroadcastSms((SmsCbMessage) message.obj); 309 return true; 310 } else { 311 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED, 312 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__CDMA, 313 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__DUPLICATE_MESSAGE); 314 } 315 return false; 316 } else { 317 final String errorMessage = 318 "handleSmsMessage got object of type: " + message.obj.getClass().getName(); 319 loge(errorMessage); 320 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_ERROR, 321 CELL_BROADCAST_MESSAGE_ERROR__TYPE__UNEXPECTED_CDMA_MESSAGE_TYPE_FROM_FWK, 322 errorMessage); 323 return false; 324 } 325 } 326 327 /** 328 * Get the maximum time for waiting location. 329 * 330 * @param message Cell broadcast message 331 * @return The maximum waiting time in second 332 */ getMaxLocationWaitingTime(SmsCbMessage message)333 protected int getMaxLocationWaitingTime(SmsCbMessage message) { 334 int maximumTime = message.getMaximumWaitingDuration(); 335 if (maximumTime == SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET) { 336 Resources res = getResources(message.getSubscriptionId()); 337 maximumTime = res.getInteger(R.integer.max_location_waiting_time); 338 } 339 return maximumTime; 340 } 341 342 /** 343 * Dispatch a Cell Broadcast message to listeners. 344 * @param message the Cell Broadcast to broadcast 345 */ handleBroadcastSms(SmsCbMessage message)346 protected void handleBroadcastSms(SmsCbMessage message) { 347 int slotIndex = message.getSlotIndex(); 348 349 // TODO: Database inserting can be time consuming, therefore this should be changed to 350 // asynchronous. 351 ContentValues cv = message.getContentValues(); 352 Uri uri = mContext.getContentResolver().insert(CellBroadcasts.CONTENT_URI, cv); 353 354 if (message.needGeoFencingCheck()) { 355 int maximumWaitingTime = getMaxLocationWaitingTime(message); 356 if (DBG) { 357 log("Requesting location for geo-fencing. serialNumber = " 358 + message.getSerialNumber() + ", maximumWaitingTime = " 359 + maximumWaitingTime); 360 } 361 362 CbSendMessageCalculator calculator = 363 mCbSendMessageCalculatorFactory.createNew(mContext, message.getGeometries()); 364 requestLocationUpdate(new LocationUpdateCallback() { 365 @Override 366 public void onLocationUpdate(@NonNull LatLng location, 367 float accuracy) { 368 if (VDBG) { 369 logd("onLocationUpdate: location=" + location 370 + ", acc=" + accuracy + ". " + getMessageString(message)); 371 } 372 performGeoFencing(message, uri, calculator, location, slotIndex, 373 accuracy); 374 if (!isMessageInAmbiguousState(calculator)) { 375 cancelLocationRequest(); 376 } 377 } 378 379 @Override 380 public void onLocationUnavailable() { 381 CellBroadcastHandler.this.onLocationUnavailable( 382 calculator, message, uri, slotIndex); 383 } 384 }, maximumWaitingTime); 385 } else { 386 if (DBG) { 387 log("Broadcast the message directly because no geo-fencing required, " 388 + " needGeoFencing = " + message.needGeoFencingCheck() + ". " 389 + getMessageString(message)); 390 } 391 broadcastMessage(message, uri, slotIndex); 392 } 393 } 394 395 /** 396 * Returns true if the message calculator is in a non-ambiguous state. 397 * 398 * </b>Note:</b> NO_COORDINATES is considered ambiguous because we received no information 399 * in this case. 400 * @param calculator the message calculator 401 * @return whether or not the message is handled 402 */ isMessageInAmbiguousState(CbSendMessageCalculator calculator)403 protected boolean isMessageInAmbiguousState(CbSendMessageCalculator calculator) { 404 return calculator.getAction() == SEND_MESSAGE_ACTION_AMBIGUOUS 405 || calculator.getAction() == SEND_MESSAGE_ACTION_NO_COORDINATES; 406 } 407 408 /** 409 * Cancels the location request 410 */ cancelLocationRequest()411 protected void cancelLocationRequest() { 412 this.mLocationRequester.cancel(); 413 } 414 415 /** 416 * When location requester cannot send anymore updates, we look at the calculated action and 417 * determine whether or not we should send it. 418 * 419 * see: {@code CellBroadcastHandler.LocationUpdateCallback#onLocationUnavailable} for more info. 420 * 421 * @param calculator the send message calculator 422 * @param message the cell broadcast message received 423 * @param uri the message's uri 424 * @param slotIndex the slot 425 */ onLocationUnavailable(CbSendMessageCalculator calculator, SmsCbMessage message, Uri uri, int slotIndex)426 protected void onLocationUnavailable(CbSendMessageCalculator calculator, SmsCbMessage message, 427 Uri uri, int slotIndex) { 428 @CbSendMessageCalculator.SendMessageAction int action = calculator.getAction(); 429 if (DBG) { 430 logd("onLocationUnavailable: action=" 431 + CbSendMessageCalculator.getActionString(action) + ". " 432 + getMessageString(message)); 433 } 434 435 if (isMessageInAmbiguousState(calculator)) { 436 /* Case 1. If we reached the end of the location time out and we are still in an 437 ambiguous state or no coordinates state, we send the message. 438 Case 2. If we don't have permissions, then no location was received and the 439 calculator's action is NO_COORDINATES, which means we also send. */ 440 broadcastGeofenceMessage(message, uri, slotIndex, calculator); 441 } else if (action == SEND_MESSAGE_ACTION_DONT_SEND) { 442 geofenceMessageNotRequired(message); 443 } 444 } 445 446 /** 447 * Check the location based on geographical scope defined in 3GPP TS 23.041 section 9.4.1.2.1. 448 * 449 * The Geographical Scope (GS) indicates the geographical area over which the Message Code 450 * is unique, and the display mode. The CBS message is not necessarily broadcast by all cells 451 * within the geographical area. When two CBS messages are received with identical Serial 452 * Numbers/Message Identifiers in two different cells, the Geographical Scope may be used to 453 * determine if the CBS messages are indeed identical. 454 * 455 * @param message The current message 456 * @param messageToCheck The older message in the database to be checked 457 * @return {@code true} if within the same area, otherwise {@code false}, which should be 458 * be considered as a new message. 459 */ isSameLocation(SmsCbMessage message, SmsCbMessage messageToCheck)460 private boolean isSameLocation(SmsCbMessage message, 461 SmsCbMessage messageToCheck) { 462 if (message.getGeographicalScope() != messageToCheck.getGeographicalScope()) { 463 return false; 464 } 465 466 // only cell wide (which means that if a message is displayed it is desirable that the 467 // message is removed from the screen when the UE selects the next cell and if any CBS 468 // message is received in the next cell it is to be regarded as "new"). 469 if (message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE 470 || message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE) { 471 return message.getLocation().isInLocationArea(messageToCheck.getLocation()); 472 } 473 474 // Service Area wide (which means that a CBS message with the same Message Code and Update 475 // Number may or may not be "new" in the next cell according to whether the next cell is 476 // in the same Service Area as the current cell) 477 if (message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE) { 478 if (!message.getLocation().getPlmn().equals(messageToCheck.getLocation().getPlmn())) { 479 return false; 480 } 481 482 return message.getLocation().getLac() != -1 483 && message.getLocation().getLac() == messageToCheck.getLocation().getLac(); 484 } 485 486 // PLMN wide (which means that the Message Code and/or Update Number must change in the 487 // next cell, of the PLMN, for the CBS message to be "new". The CBS message is only relevant 488 // to the PLMN in which it is broadcast, so any change of PLMN (including a change to 489 // another PLMN which is an ePLMN) means the CBS message is "new") 490 if (message.getGeographicalScope() == SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE) { 491 return !TextUtils.isEmpty(message.getLocation().getPlmn()) 492 && message.getLocation().getPlmn().equals( 493 messageToCheck.getLocation().getPlmn()); 494 } 495 496 return false; 497 } 498 499 /** 500 * Check if the message is a duplicate 501 * 502 * @param message Cell broadcast message 503 * @return {@code true} if this message is a duplicate 504 */ 505 @VisibleForTesting isDuplicate(SmsCbMessage message)506 public boolean isDuplicate(SmsCbMessage message) { 507 if (!mEnableDuplicateDetection) { 508 log("Duplicate detection was disabled for debugging purposes."); 509 return false; 510 } 511 512 // Find the cell broadcast message identify by the message identifier and serial number 513 // and is not broadcasted. 514 String where = CellBroadcasts.RECEIVED_TIME + ">?"; 515 516 Resources res = getResources(message.getSubscriptionId()); 517 518 // Only consider cell broadcast messages received within certain period. 519 // By default it's 24 hours. 520 long expirationDuration = res.getInteger(R.integer.message_expiration_time); 521 long dupCheckTime = System.currentTimeMillis() - expirationDuration; 522 523 // Some carriers require reset duplication detection after airplane mode or reboot. 524 if (res.getBoolean(R.bool.reset_on_power_cycle_or_airplane_mode)) { 525 dupCheckTime = Long.max(dupCheckTime, mLastAirplaneModeTime); 526 dupCheckTime = Long.max(dupCheckTime, 527 System.currentTimeMillis() - SystemClock.elapsedRealtime()); 528 } 529 530 List<SmsCbMessage> cbMessages = new ArrayList<>(); 531 532 try (Cursor cursor = mContext.getContentResolver().query(CellBroadcasts.CONTENT_URI, 533 CellBroadcastProvider.QUERY_COLUMNS, 534 where, 535 new String[] {Long.toString(dupCheckTime)}, 536 null)) { 537 if (cursor != null) { 538 while (cursor.moveToNext()) { 539 cbMessages.add(SmsCbMessage.createFromCursor(cursor)); 540 } 541 } 542 } 543 544 boolean compareMessageBody = res.getBoolean(R.bool.duplicate_compare_body); 545 boolean compareServiceCategory = res.getBoolean(R.bool.duplicate_compare_service_category); 546 boolean crossSimDuplicateDetection = res.getBoolean(R.bool.cross_sim_duplicate_detection); 547 548 log("Found " + cbMessages.size() + " messages since " 549 + DateFormat.getDateTimeInstance().format(dupCheckTime)); 550 log("compareMessageBody=" + compareMessageBody + ", compareServiceCategory=" 551 + compareServiceCategory + ", crossSimDuplicateDetection=" 552 + crossSimDuplicateDetection); 553 for (SmsCbMessage messageToCheck : cbMessages) { 554 // If messages are from different slots, then we only compare the message body. 555 if (VDBG) log("Checking the message " + messageToCheck); 556 if (crossSimDuplicateDetection 557 && message.getSlotIndex() != messageToCheck.getSlotIndex()) { 558 if (TextUtils.equals(message.getMessageBody(), messageToCheck.getMessageBody())) { 559 log("Duplicate message detected from different slot. " + message); 560 return true; 561 } 562 if (VDBG) log("Not from the same slot."); 563 } else { 564 // Check serial number if message is from the same carrier. 565 if (message.getSerialNumber() != messageToCheck.getSerialNumber()) { 566 if (VDBG) log("Serial number does not match."); 567 // Not a dup. Check next one. 568 continue; 569 } 570 571 // ETWS primary / secondary should be treated differently. 572 if (message.isEtwsMessage() && messageToCheck.isEtwsMessage() 573 && message.getEtwsWarningInfo().isPrimary() 574 != messageToCheck.getEtwsWarningInfo().isPrimary()) { 575 if (VDBG) log("ETWS primary/secondary does not match."); 576 // Not a dup. Check next one. 577 continue; 578 } 579 580 // Check if the message category is different. 581 if (compareServiceCategory 582 && message.getServiceCategory() != messageToCheck.getServiceCategory() 583 && !Objects.equals(mServiceCategoryCrossRATMap.get( 584 message.getServiceCategory()), messageToCheck.getServiceCategory()) 585 && !Objects.equals(mServiceCategoryCrossRATMap.get( 586 messageToCheck.getServiceCategory()), 587 message.getServiceCategory())) { 588 if (VDBG) log("Category does not match."); 589 // Not a dup. Check next one. 590 continue; 591 } 592 593 // Check if the message location is different. Note this is only applicable to 594 // 3GPP format cell broadcast messages. 595 if (message.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP 596 && messageToCheck.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP 597 && !isSameLocation(message, messageToCheck)) { 598 if (VDBG) log("Location does not match."); 599 // Not a dup. Check next one. 600 continue; 601 } 602 603 // Compare message body if needed. 604 if (!compareMessageBody || TextUtils.equals( 605 message.getMessageBody(), messageToCheck.getMessageBody())) { 606 log("Duplicate message detected. " + message); 607 return true; 608 } else { 609 if (VDBG) log("Body does not match."); 610 } 611 } 612 } 613 614 log("Not a duplicate message. " + message); 615 return false; 616 } 617 618 /** 619 * Perform a geo-fencing check for {@code message}. Broadcast the {@code message} if the 620 * {@code location} is inside the {@code broadcastArea}. 621 * @param message the message need to geo-fencing check 622 * @param uri the message's uri 623 * @param calculator the message calculator 624 * @param location current location 625 * @param slotIndex the index of the slot 626 * @param accuracy the accuracy of the coordinate given in meters 627 */ performGeoFencing(SmsCbMessage message, Uri uri, CbSendMessageCalculator calculator, LatLng location, int slotIndex, float accuracy)628 protected void performGeoFencing(SmsCbMessage message, Uri uri, 629 CbSendMessageCalculator calculator, LatLng location, int slotIndex, float accuracy) { 630 631 logd(calculator.toString() + ", current action=" 632 + CbSendMessageCalculator.getActionString(calculator.getAction())); 633 634 if (calculator.getAction() == SEND_MESSAGE_ACTION_SENT) { 635 if (VDBG) { 636 logd("performGeoFencing:" + getMessageString(message)); 637 } 638 return; 639 } 640 641 if (uri != null) { 642 ContentValues cv = new ContentValues(); 643 cv.put(CellBroadcasts.LOCATION_CHECK_TIME, System.currentTimeMillis()); 644 mContext.getContentResolver().update(CellBroadcasts.CONTENT_URI, cv, 645 CellBroadcasts._ID + "=?", new String[] {uri.getLastPathSegment()}); 646 } 647 648 649 calculator.addCoordinate(location, accuracy); 650 651 if (VDBG) { 652 logd("Device location new action = " 653 + CbSendMessageCalculator.getActionString(calculator.getAction()) 654 + ", threshold = " + calculator.getThreshold() 655 + ", geos=" + CbGeoUtils.encodeGeometriesToString(calculator.getFences()) 656 + ". " + getMessageString(message)); 657 } 658 659 if (calculator.getAction() == SEND_MESSAGE_ACTION_SEND) { 660 broadcastGeofenceMessage(message, uri, slotIndex, calculator); 661 return; 662 } 663 } 664 geofenceMessageNotRequired(SmsCbMessage message)665 protected void geofenceMessageNotRequired(SmsCbMessage message) { 666 if (message.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP) { 667 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED, 668 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__GSM, 669 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__GEOFENCED_MESSAGE); 670 } else if (message.getMessageFormat() == SmsCbMessage.MESSAGE_FORMAT_3GPP2) { 671 CellBroadcastStatsLog.write(CellBroadcastStatsLog.CB_MESSAGE_FILTERED, 672 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__TYPE__CDMA, 673 CellBroadcastStatsLog.CELL_BROADCAST_MESSAGE_FILTERED__FILTER__GEOFENCED_MESSAGE); 674 } 675 676 sendMessage(EVENT_BROADCAST_NOT_REQUIRED); 677 } 678 679 /** 680 * Requests a stream of updates for {@code maximumWaitTimeSec} seconds. 681 * @param callback the callback used to communicate back to the caller 682 * @param maximumWaitTimeSec the maximum wait time of this request. If location is not updated 683 * within the maximum wait time, {@code callback#onLocationUnavailable()} will be called. 684 */ requestLocationUpdate(LocationUpdateCallback callback, int maximumWaitTimeSec)685 protected void requestLocationUpdate(LocationUpdateCallback callback, int maximumWaitTimeSec) { 686 mLocationRequester.requestLocationUpdate(callback, maximumWaitTimeSec); 687 } 688 689 /** 690 * Get the subscription ID for a phone ID, or INVALID_SUBSCRIPTION_ID if the phone does not 691 * have an active sub 692 * @param phoneId the phoneId to use 693 * @return the associated sub id 694 */ getSubIdForPhone(Context context, int phoneId)695 protected static int getSubIdForPhone(Context context, int phoneId) { 696 SubscriptionManager subMan = 697 (SubscriptionManager) context.getSystemService( 698 Context.TELEPHONY_SUBSCRIPTION_SERVICE); 699 int[] subIds = subMan.getSubscriptionIds(phoneId); 700 if (subIds != null) { 701 return subIds[0]; 702 } else { 703 return SubscriptionManager.INVALID_SUBSCRIPTION_ID; 704 } 705 } 706 707 /** 708 * Put the phone ID and sub ID into an intent as extras. 709 */ putPhoneIdAndSubIdExtra(Context context, Intent intent, int phoneId)710 public static void putPhoneIdAndSubIdExtra(Context context, Intent intent, int phoneId) { 711 int subId = getSubIdForPhone(context, phoneId); 712 if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 713 intent.putExtra("subscription", subId); 714 intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId); 715 } 716 intent.putExtra("phone", phoneId); 717 intent.putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, phoneId); 718 } 719 720 /** 721 * Call when dealing with messages that are checked against a geofence. 722 * 723 * @param message the message being broadcast 724 * @param messageUri the message uri 725 * @param slotIndex the slot index 726 * @param calculator the messages send message calculator 727 */ broadcastGeofenceMessage(@onNull SmsCbMessage message, @Nullable Uri messageUri, int slotIndex, CbSendMessageCalculator calculator)728 protected void broadcastGeofenceMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri, 729 int slotIndex, CbSendMessageCalculator calculator) { 730 // Check that the message wasn't already SENT 731 if (calculator.getAction() == CbSendMessageCalculator.SEND_MESSAGE_ACTION_SENT) { 732 return; 733 } 734 735 if (VDBG) { 736 logd("broadcastGeofenceMessage: mark as sent. " + getMessageString(message)); 737 } 738 // Mark the message as SENT 739 calculator.markAsSent(); 740 741 // Broadcast the message 742 broadcastMessage(message, messageUri, slotIndex); 743 } 744 745 /** 746 * Broadcast the {@code message} to the applications. 747 * @param message a message need to broadcast 748 * @param messageUri message's uri 749 */ broadcastMessage(@onNull SmsCbMessage message, @Nullable Uri messageUri, int slotIndex)750 protected void broadcastMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri, 751 int slotIndex) { 752 String msg; 753 Intent intent; 754 if (VDBG) { 755 logd("broadcastMessage: " + getMessageString(message)); 756 } 757 758 if (message.isEmergencyMessage()) { 759 msg = "Dispatching emergency SMS CB, SmsCbMessage is: " + message; 760 log(msg); 761 mLocalLog.log(msg); 762 intent = new Intent(Telephony.Sms.Intents.ACTION_SMS_EMERGENCY_CB_RECEIVED); 763 //Emergency alerts need to be delivered with high priority 764 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 765 766 intent.putExtra(EXTRA_MESSAGE, message); 767 putPhoneIdAndSubIdExtra(mContext, intent, slotIndex); 768 769 if (IS_DEBUGGABLE) { 770 // Send additional broadcast intent to the specified package. This is only for sl4a 771 // automation tests. 772 String[] testPkgs = mContext.getResources().getStringArray( 773 R.array.test_cell_broadcast_receiver_packages); 774 if (testPkgs != null) { 775 Intent additionalIntent = new Intent(intent); 776 for (String pkg : testPkgs) { 777 additionalIntent.setPackage(pkg); 778 mLocalLog.log("intent=" + intent + " package=" + pkg); 779 mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast( 780 intent, null, (Bundle) null, null, getHandler(), 781 Activity.RESULT_OK, null, null); 782 783 } 784 } 785 } 786 787 List<String> pkgs = new ArrayList<>(); 788 pkgs.add(getDefaultCBRPackageName(mContext, intent)); 789 pkgs.addAll(Arrays.asList(mContext.getResources().getStringArray( 790 R.array.additional_cell_broadcast_receiver_packages))); 791 if (pkgs != null) { 792 mReceiverCount.addAndGet(pkgs.size()); 793 for (String pkg : pkgs) { 794 // Explicitly send the intent to all the configured cell broadcast receivers. 795 intent.setPackage(pkg); 796 mLocalLog.log("intent=" + intent + " package=" + pkg); 797 mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast( 798 intent, null, (Bundle) null, mOrderedBroadcastReceiver, getHandler(), 799 Activity.RESULT_OK, null, null); 800 } 801 } 802 } else { 803 msg = "Dispatching SMS CB, SmsCbMessage is: " + message; 804 log(msg); 805 mLocalLog.log(msg); 806 // Send implicit intent since there are various 3rd party carrier apps listen to 807 // this intent. 808 809 mReceiverCount.incrementAndGet(); 810 CellBroadcastIntents.sendSmsCbReceivedBroadcast( 811 mContext, UserHandle.ALL, message, mOrderedBroadcastReceiver, getHandler(), 812 Activity.RESULT_OK, slotIndex); 813 } 814 815 if (messageUri != null) { 816 ContentValues cv = new ContentValues(); 817 cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1); 818 mContext.getContentResolver().update(CellBroadcasts.CONTENT_URI, cv, 819 CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()}); 820 } 821 } 822 823 /** 824 * Checks if the app's path starts with CB_APEX_PATH 825 */ isAppInCBApexOrAlternativeApp(ApplicationInfo appInfo)826 private static boolean isAppInCBApexOrAlternativeApp(ApplicationInfo appInfo) { 827 return appInfo.sourceDir.startsWith(CB_APEX_PATH) || 828 appInfo.sourceDir.contains(CB_APP_PLATFORM_NAME); 829 } 830 831 /** 832 * Find the name of the default CBR package. The criteria is that it belongs to CB apex and 833 * handles the given intent. 834 */ getDefaultCBRPackageName(Context context, Intent intent)835 static String getDefaultCBRPackageName(Context context, Intent intent) { 836 PackageManager packageManager = context.getPackageManager(); 837 List<ResolveInfo> cbrPackages = packageManager.queryBroadcastReceivers(intent, 0); 838 839 // remove apps that don't live in the CellBroadcast apex 840 cbrPackages.removeIf(info -> 841 !isAppInCBApexOrAlternativeApp(info.activityInfo.applicationInfo)); 842 843 if (cbrPackages.isEmpty()) { 844 Log.e(TAG, "getCBRPackageNames: no package found"); 845 return null; 846 } 847 848 if (cbrPackages.size() > 1) { 849 // multiple apps found, log an error but continue 850 Log.e(TAG, "Found > 1 APK in CB apex that can resolve " + intent.getAction() + ": " 851 + cbrPackages.stream() 852 .map(info -> info.activityInfo.applicationInfo.packageName) 853 .collect(Collectors.joining(", "))); 854 } 855 856 // Assume the first ResolveInfo is the one we're looking for 857 ResolveInfo info = cbrPackages.get(0); 858 return info.activityInfo.applicationInfo.packageName; 859 } 860 861 /** 862 * Get the device resource based on SIM 863 * 864 * @param subId Subscription index 865 * 866 * @return The resource 867 */ getResources(int subId)868 public @NonNull Resources getResources(int subId) { 869 if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID 870 || !SubscriptionManager.isValidSubscriptionId(subId)) { 871 return mContext.getResources(); 872 } 873 874 if (mResourcesCache.containsKey(subId)) { 875 return mResourcesCache.get(subId); 876 } 877 878 return SubscriptionManager.getResourcesForSubId(mContext, subId); 879 } 880 881 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)882 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 883 pw.println("CellBroadcastHandler:"); 884 mLocalLog.dump(fd, pw, args); 885 pw.flush(); 886 try { 887 super.dump(fd, pw, args); 888 } catch (NullPointerException e) { 889 // StateMachine.dump() throws a NPE if there is no current state in the stack. Since 890 // StateMachine is defined in the framework and CBS is updated through mailine, we 891 // catch the NPE here as well as fixing the exception in the framework. 892 pw.println("StateMachine: no state info"); 893 } 894 } 895 896 /** The callback interface of a location request. */ 897 public interface LocationUpdateCallback { 898 /** 899 * Called when the location update is available. 900 * @param location a location in (latitude, longitude) format. 901 * @param accuracy the accuracy of the location given from location manager. Given in 902 * meters. 903 */ onLocationUpdate(@onNull LatLng location, float accuracy)904 void onLocationUpdate(@NonNull LatLng location, float accuracy); 905 906 /** 907 * This is called in the following scenarios: 908 * 1. The max time limit of the LocationRequester was reached, and consequently, 909 * no more location updates will be sent. 910 * 2. The service does not have permission to request a location update. 911 * 3. The LocationRequester was explicitly cancelled. 912 */ onLocationUnavailable()913 void onLocationUnavailable(); 914 } 915 916 private static final class LocationRequester { 917 /** 918 * Fused location provider, which means GPS plus network based providers (cell, wifi, etc..) 919 */ 920 //TODO: Should make LocationManager.FUSED_PROVIDER system API in S. 921 private static final String FUSED_PROVIDER = "fused"; 922 923 /** 924 * The interval in which location requests will be sent. 925 * see more: <code>LocationRequest#setInterval</code> 926 */ 927 private static final long LOCATION_REQUEST_INTERVAL_MILLIS = 1000; 928 929 private final LocationManager mLocationManager; 930 private final List<LocationUpdateCallback> mCallbacks; 931 private final HandlerHelper mHandlerHelper; 932 private final Context mContext; 933 private final LocationListener mLocationListener; 934 935 private boolean mLocationUpdateInProgress; 936 private final Runnable mLocationUnavailable; 937 private final String mDebugTag; 938 LocationRequester(Context context, LocationManager locationManager, HandlerHelper handlerHelper, String debugTag)939 LocationRequester(Context context, LocationManager locationManager, 940 HandlerHelper handlerHelper, String debugTag) { 941 mLocationManager = locationManager; 942 mCallbacks = new ArrayList<>(); 943 mContext = context; 944 mHandlerHelper = handlerHelper; 945 mLocationUpdateInProgress = false; 946 mLocationUnavailable = this::onLocationUnavailable; 947 948 // Location request did not cancel itself when using this::onLocationListener 949 mLocationListener = this::onLocationUpdate; 950 mDebugTag = debugTag; 951 } 952 953 /** 954 * Requests a stream of updates for {@code maximumWaitTimeSec} seconds. 955 * @param callback the callback used to communicate back to the caller 956 * @param maximumWaitTimeSec the maximum wait time of this request. If location is not 957 * updated within the maximum wait time, 958 * {@code callback#onLocationUnavailable()} will be called. 959 */ requestLocationUpdate(@onNull LocationUpdateCallback callback, int maximumWaitTimeSec)960 void requestLocationUpdate(@NonNull LocationUpdateCallback callback, 961 int maximumWaitTimeSec) { 962 mHandlerHelper.post(() -> requestLocationUpdateInternal(callback, maximumWaitTimeSec)); 963 } 964 onLocationUpdate(@onNull Location location)965 private void onLocationUpdate(@NonNull Location location) { 966 if (location == null) { 967 /* onLocationUpdate should neverreceive a null location, but, covering all of our 968 bases here. */ 969 Log.wtf(mDebugTag, "Location is never supposed to be null"); 970 return; 971 } 972 973 LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); 974 float accuracy = location.getAccuracy(); 975 if (DBG) { 976 Log.d(mDebugTag, "onLocationUpdate: received location update"); 977 } 978 979 for (LocationUpdateCallback callback : mCallbacks) { 980 callback.onLocationUpdate(latLng, accuracy); 981 } 982 } 983 onLocationUnavailable()984 private void onLocationUnavailable() { 985 Log.d(mDebugTag, "onLocationUnavailable: called"); 986 locationRequesterCycleComplete(); 987 } 988 989 /* This should only be called if all of the messages are handled. */ cancel()990 public void cancel() { 991 if (mLocationUpdateInProgress) { 992 Log.d(mDebugTag, "cancel: location update in progress"); 993 mHandlerHelper.removeCallbacks(mLocationUnavailable); 994 locationRequesterCycleComplete(); 995 } else { 996 Log.d(mDebugTag, "cancel: location update NOT in progress"); 997 } 998 } 999 locationRequesterCycleComplete()1000 private void locationRequesterCycleComplete() { 1001 try { 1002 for (LocationUpdateCallback callback : mCallbacks) { 1003 callback.onLocationUnavailable(); 1004 } 1005 } finally { 1006 mLocationManager.removeUpdates(mLocationListener); 1007 // Reset the state of location requester for the next request 1008 mCallbacks.clear(); 1009 mLocationUpdateInProgress = false; 1010 } 1011 } 1012 requestLocationUpdateInternal(@onNull LocationUpdateCallback callback, int maximumWaitTimeS)1013 private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback, 1014 int maximumWaitTimeS) { 1015 if (DBG) Log.d(mDebugTag, "requestLocationUpdate"); 1016 if (!hasPermission(ACCESS_FINE_LOCATION) && !hasPermission(ACCESS_COARSE_LOCATION)) { 1017 if (DBG) { 1018 Log.e(mDebugTag, 1019 "Can't request location update because of no location permission"); 1020 } 1021 callback.onLocationUnavailable(); 1022 return; 1023 } 1024 1025 if (!mLocationUpdateInProgress) { 1026 try { 1027 // If the user does not turn on location, immediately report location 1028 // unavailable. 1029 if (!mLocationManager.isLocationEnabled()) { 1030 Log.d(mDebugTag, "Location is turned off."); 1031 callback.onLocationUnavailable(); 1032 return; 1033 } 1034 1035 /* We will continue to send updates until the location timeout is reached. The 1036 location timeout case is handled through onLocationUnavailable. */ 1037 LocationRequest request = LocationRequest.create() 1038 .setProvider(FUSED_PROVIDER) 1039 .setQuality(LocationRequest.ACCURACY_FINE) 1040 .setInterval(LOCATION_REQUEST_INTERVAL_MILLIS); 1041 if (DBG) { 1042 Log.d(mDebugTag, "Location request=" + request); 1043 } 1044 mLocationManager.requestLocationUpdates(request, 1045 new HandlerExecutor(mHandlerHelper.getHandler()), 1046 mLocationListener); 1047 1048 // TODO: Remove the following workaround in S. We need to enforce the timeout 1049 // before location manager adds the support for timeout value which is less 1050 // than 30 seconds. After that we can rely on location manager's timeout 1051 // mechanism. 1052 mHandlerHelper.postDelayed(mLocationUnavailable, 1053 TimeUnit.SECONDS.toMillis(maximumWaitTimeS)); 1054 } catch (IllegalArgumentException e) { 1055 Log.e(mDebugTag, "Cannot get current location. e=" + e); 1056 callback.onLocationUnavailable(); 1057 return; 1058 } 1059 mLocationUpdateInProgress = true; 1060 } 1061 mCallbacks.add(callback); 1062 } 1063 hasPermission(String permission)1064 private boolean hasPermission(String permission) { 1065 // TODO: remove the check. This will always return true because cell broadcast service 1066 // is running under the UID Process.NETWORK_STACK_UID, which is below 10000. It will be 1067 // automatically granted with all runtime permissions. 1068 return mContext.checkPermission(permission, Process.myPid(), Process.myUid()) 1069 == PackageManager.PERMISSION_GRANTED; 1070 } 1071 } 1072 1073 /** 1074 * Provides message identifiers that are helpful when logging messages. 1075 * 1076 * @param message the message to log 1077 * @return a helpful message 1078 */ getMessageString(SmsCbMessage message)1079 protected static String getMessageString(SmsCbMessage message) { 1080 return "msg=(" 1081 + message.getServiceCategory() + "," 1082 + message.getSerialNumber() + ")"; 1083 } 1084 1085 1086 /** 1087 * Wraps the {@code Handler} in order to mock the methods. 1088 */ 1089 @VisibleForTesting 1090 public static class HandlerHelper { 1091 1092 private final Handler mHandler; 1093 HandlerHelper(@onNull final Handler handler)1094 public HandlerHelper(@NonNull final Handler handler) { 1095 mHandler = handler; 1096 } 1097 1098 /** 1099 * Posts {@code r} to {@code handler} with a delay of {@code delayMillis} 1100 * 1101 * @param r the runnable callback 1102 * @param delayMillis the number of milliseconds to delay 1103 */ postDelayed(Runnable r, long delayMillis)1104 public void postDelayed(Runnable r, long delayMillis) { 1105 mHandler.postDelayed(r, delayMillis); 1106 } 1107 1108 /** 1109 * Posts {@code r} to the underlying handler 1110 * 1111 * @param r the runnable callback 1112 */ post(Runnable r)1113 public void post(Runnable r) { 1114 mHandler.post(r); 1115 } 1116 1117 /** 1118 * Gets the underlying handler 1119 * @return the handler 1120 */ getHandler()1121 public Handler getHandler() { 1122 return mHandler; 1123 } 1124 1125 /** 1126 * Remove any pending posts of Runnable r that are in the message queue. 1127 */ removeCallbacks(Runnable r)1128 public void removeCallbacks(Runnable r) { 1129 mHandler.removeCallbacks(r); 1130 } 1131 } 1132 } 1133