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.HandlerExecutor; 50 import android.os.Looper; 51 import android.os.Message; 52 import android.os.Process; 53 import android.os.SystemClock; 54 import android.os.SystemProperties; 55 import android.os.UserHandle; 56 import android.provider.Telephony; 57 import android.provider.Telephony.CellBroadcasts; 58 import android.telephony.CbGeoUtils.LatLng; 59 import android.telephony.CellBroadcastIntents; 60 import android.telephony.SmsCbMessage; 61 import android.telephony.SubscriptionManager; 62 import android.telephony.cdma.CdmaSmsCbProgramData; 63 import android.text.TextUtils; 64 import android.util.LocalLog; 65 import android.util.Log; 66 67 import com.android.internal.annotations.VisibleForTesting; 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 */ 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); 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 mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast( 779 intent, null, (Bundle) null, null, getHandler(), 780 Activity.RESULT_OK, null, null); 781 782 } 783 } 784 } 785 786 List<String> pkgs = new ArrayList<>(); 787 pkgs.add(getDefaultCBRPackageName(mContext, intent)); 788 pkgs.addAll(Arrays.asList(mContext.getResources().getStringArray( 789 R.array.additional_cell_broadcast_receiver_packages))); 790 if (pkgs != null) { 791 mReceiverCount.addAndGet(pkgs.size()); 792 for (String pkg : pkgs) { 793 // Explicitly send the intent to all the configured cell broadcast receivers. 794 intent.setPackage(pkg); 795 mContext.createContextAsUser(UserHandle.ALL, 0).sendOrderedBroadcast( 796 intent, null, (Bundle) null, mOrderedBroadcastReceiver, getHandler(), 797 Activity.RESULT_OK, null, null); 798 } 799 } 800 } else { 801 msg = "Dispatching SMS CB, SmsCbMessage is: " + message; 802 log(msg); 803 mLocalLog.log(msg); 804 // Send implicit intent since there are various 3rd party carrier apps listen to 805 // this intent. 806 807 mReceiverCount.incrementAndGet(); 808 CellBroadcastIntents.sendSmsCbReceivedBroadcast( 809 mContext, UserHandle.ALL, message, mOrderedBroadcastReceiver, getHandler(), 810 Activity.RESULT_OK, slotIndex); 811 } 812 813 if (messageUri != null) { 814 ContentValues cv = new ContentValues(); 815 cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1); 816 mContext.getContentResolver().update(CellBroadcasts.CONTENT_URI, cv, 817 CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()}); 818 } 819 } 820 821 /** 822 * Checks if the app's path starts with CB_APEX_PATH 823 */ isAppInCBApexOrAlternativeApp(ApplicationInfo appInfo)824 private static boolean isAppInCBApexOrAlternativeApp(ApplicationInfo appInfo) { 825 return appInfo.sourceDir.startsWith(CB_APEX_PATH) || 826 appInfo.sourceDir.contains(CB_APP_PLATFORM_NAME); 827 } 828 829 /** 830 * Find the name of the default CBR package. The criteria is that it belongs to CB apex and 831 * handles the given intent. 832 */ getDefaultCBRPackageName(Context context, Intent intent)833 static String getDefaultCBRPackageName(Context context, Intent intent) { 834 PackageManager packageManager = context.getPackageManager(); 835 List<ResolveInfo> cbrPackages = packageManager.queryBroadcastReceivers(intent, 0); 836 837 // remove apps that don't live in the CellBroadcast apex 838 cbrPackages.removeIf(info -> 839 !isAppInCBApexOrAlternativeApp(info.activityInfo.applicationInfo)); 840 841 if (cbrPackages.isEmpty()) { 842 Log.e(TAG, "getCBRPackageNames: no package found"); 843 return null; 844 } 845 846 if (cbrPackages.size() > 1) { 847 // multiple apps found, log an error but continue 848 Log.e(TAG, "Found > 1 APK in CB apex that can resolve " + intent.getAction() + ": " 849 + cbrPackages.stream() 850 .map(info -> info.activityInfo.applicationInfo.packageName) 851 .collect(Collectors.joining(", "))); 852 } 853 854 // Assume the first ResolveInfo is the one we're looking for 855 ResolveInfo info = cbrPackages.get(0); 856 return info.activityInfo.applicationInfo.packageName; 857 } 858 859 /** 860 * Get the device resource based on SIM 861 * 862 * @param subId Subscription index 863 * 864 * @return The resource 865 */ getResources(int subId)866 public @NonNull Resources getResources(int subId) { 867 if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID 868 || !SubscriptionManager.isValidSubscriptionId(subId)) { 869 return mContext.getResources(); 870 } 871 872 if (mResourcesCache.containsKey(subId)) { 873 return mResourcesCache.get(subId); 874 } 875 876 Resources res = SubscriptionManager.getResourcesForSubId(mContext, subId); 877 mResourcesCache.put(subId, res); 878 879 return res; 880 } 881 882 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)883 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 884 pw.println("CellBroadcastHandler:"); 885 mLocalLog.dump(fd, pw, args); 886 pw.flush(); 887 } 888 889 /** The callback interface of a location request. */ 890 public interface LocationUpdateCallback { 891 /** 892 * Called when the location update is available. 893 * @param location a location in (latitude, longitude) format. 894 * @param accuracy the accuracy of the location given from location manager. Given in 895 * meters. 896 */ onLocationUpdate(@onNull LatLng location, float accuracy)897 void onLocationUpdate(@NonNull LatLng location, float accuracy); 898 899 /** 900 * This is called in the following scenarios: 901 * 1. The max time limit of the LocationRequester was reached, and consequently, 902 * no more location updates will be sent. 903 * 2. The service does not have permission to request a location update. 904 * 3. The LocationRequester was explicitly cancelled. 905 */ onLocationUnavailable()906 void onLocationUnavailable(); 907 } 908 909 private static final class LocationRequester { 910 /** 911 * Fused location provider, which means GPS plus network based providers (cell, wifi, etc..) 912 */ 913 //TODO: Should make LocationManager.FUSED_PROVIDER system API in S. 914 private static final String FUSED_PROVIDER = "fused"; 915 916 /** 917 * The interval in which location requests will be sent. 918 * see more: <code>LocationRequest#setInterval</code> 919 */ 920 private static final long LOCATION_REQUEST_INTERVAL_MILLIS = 1000; 921 922 private final LocationManager mLocationManager; 923 private final List<LocationUpdateCallback> mCallbacks; 924 private final HandlerHelper mHandlerHelper; 925 private final Context mContext; 926 private final LocationListener mLocationListener; 927 928 private boolean mLocationUpdateInProgress; 929 private final Runnable mLocationUnavailable; 930 private final String mDebugTag; 931 LocationRequester(Context context, LocationManager locationManager, HandlerHelper handlerHelper, String debugTag)932 LocationRequester(Context context, LocationManager locationManager, 933 HandlerHelper handlerHelper, String debugTag) { 934 mLocationManager = locationManager; 935 mCallbacks = new ArrayList<>(); 936 mContext = context; 937 mHandlerHelper = handlerHelper; 938 mLocationUpdateInProgress = false; 939 mLocationUnavailable = this::onLocationUnavailable; 940 941 // Location request did not cancel itself when using this::onLocationListener 942 mLocationListener = this::onLocationUpdate; 943 mDebugTag = debugTag; 944 } 945 946 /** 947 * Requests a stream of updates for {@code maximumWaitTimeSec} seconds. 948 * @param callback the callback used to communicate back to the caller 949 * @param maximumWaitTimeSec the maximum wait time of this request. If location is not 950 * updated within the maximum wait time, 951 * {@code callback#onLocationUnavailable()} will be called. 952 */ requestLocationUpdate(@onNull LocationUpdateCallback callback, int maximumWaitTimeSec)953 void requestLocationUpdate(@NonNull LocationUpdateCallback callback, 954 int maximumWaitTimeSec) { 955 mHandlerHelper.post(() -> requestLocationUpdateInternal(callback, maximumWaitTimeSec)); 956 } 957 onLocationUpdate(@onNull Location location)958 private void onLocationUpdate(@NonNull Location location) { 959 if (location == null) { 960 /* onLocationUpdate should neverreceive a null location, but, covering all of our 961 bases here. */ 962 Log.wtf(mDebugTag, "Location is never supposed to be null"); 963 return; 964 } 965 966 LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude()); 967 float accuracy = location.getAccuracy(); 968 if (DBG) { 969 Log.d(mDebugTag, "onLocationUpdate: received location update"); 970 } 971 972 for (LocationUpdateCallback callback : mCallbacks) { 973 callback.onLocationUpdate(latLng, accuracy); 974 } 975 } 976 onLocationUnavailable()977 private void onLocationUnavailable() { 978 Log.d(mDebugTag, "onLocationUnavailable: called"); 979 locationRequesterCycleComplete(); 980 } 981 982 /* This should only be called if all of the messages are handled. */ cancel()983 public void cancel() { 984 if (mLocationUpdateInProgress) { 985 Log.d(mDebugTag, "cancel: location update in progress"); 986 mHandlerHelper.removeCallbacks(mLocationUnavailable); 987 locationRequesterCycleComplete(); 988 } else { 989 Log.d(mDebugTag, "cancel: location update NOT in progress"); 990 } 991 } 992 locationRequesterCycleComplete()993 private void locationRequesterCycleComplete() { 994 try { 995 for (LocationUpdateCallback callback : mCallbacks) { 996 callback.onLocationUnavailable(); 997 } 998 } finally { 999 mLocationManager.removeUpdates(mLocationListener); 1000 // Reset the state of location requester for the next request 1001 mCallbacks.clear(); 1002 mLocationUpdateInProgress = false; 1003 } 1004 } 1005 requestLocationUpdateInternal(@onNull LocationUpdateCallback callback, int maximumWaitTimeS)1006 private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback, 1007 int maximumWaitTimeS) { 1008 if (DBG) Log.d(mDebugTag, "requestLocationUpdate"); 1009 if (!hasPermission(ACCESS_FINE_LOCATION) && !hasPermission(ACCESS_COARSE_LOCATION)) { 1010 if (DBG) { 1011 Log.e(mDebugTag, 1012 "Can't request location update because of no location permission"); 1013 } 1014 callback.onLocationUnavailable(); 1015 return; 1016 } 1017 1018 if (!mLocationUpdateInProgress) { 1019 try { 1020 // If the user does not turn on location, immediately report location 1021 // unavailable. 1022 if (!mLocationManager.isLocationEnabled()) { 1023 Log.d(mDebugTag, "Location is turned off."); 1024 callback.onLocationUnavailable(); 1025 return; 1026 } 1027 1028 /* We will continue to send updates until the location timeout is reached. The 1029 location timeout case is handled through onLocationUnavailable. */ 1030 LocationRequest request = LocationRequest.create() 1031 .setProvider(FUSED_PROVIDER) 1032 .setQuality(LocationRequest.ACCURACY_FINE) 1033 .setInterval(LOCATION_REQUEST_INTERVAL_MILLIS); 1034 if (DBG) { 1035 Log.d(mDebugTag, "Location request=" + request); 1036 } 1037 mLocationManager.requestLocationUpdates(request, 1038 new HandlerExecutor(mHandlerHelper.getHandler()), 1039 mLocationListener); 1040 1041 // TODO: Remove the following workaround in S. We need to enforce the timeout 1042 // before location manager adds the support for timeout value which is less 1043 // than 30 seconds. After that we can rely on location manager's timeout 1044 // mechanism. 1045 mHandlerHelper.postDelayed(mLocationUnavailable, 1046 TimeUnit.SECONDS.toMillis(maximumWaitTimeS)); 1047 } catch (IllegalArgumentException e) { 1048 Log.e(mDebugTag, "Cannot get current location. e=" + e); 1049 callback.onLocationUnavailable(); 1050 return; 1051 } 1052 mLocationUpdateInProgress = true; 1053 } 1054 mCallbacks.add(callback); 1055 } 1056 hasPermission(String permission)1057 private boolean hasPermission(String permission) { 1058 // TODO: remove the check. This will always return true because cell broadcast service 1059 // is running under the UID Process.NETWORK_STACK_UID, which is below 10000. It will be 1060 // automatically granted with all runtime permissions. 1061 return mContext.checkPermission(permission, Process.myPid(), Process.myUid()) 1062 == PackageManager.PERMISSION_GRANTED; 1063 } 1064 } 1065 1066 /** 1067 * Provides message identifiers that are helpful when logging messages. 1068 * 1069 * @param message the message to log 1070 * @return a helpful message 1071 */ getMessageString(SmsCbMessage message)1072 protected static String getMessageString(SmsCbMessage message) { 1073 return "msg=(" 1074 + message.getServiceCategory() + "," 1075 + message.getSerialNumber() + ")"; 1076 } 1077 1078 1079 /** 1080 * Wraps the {@code Handler} in order to mock the methods. 1081 */ 1082 @VisibleForTesting 1083 public static class HandlerHelper { 1084 1085 private final Handler mHandler; 1086 HandlerHelper(@onNull final Handler handler)1087 public HandlerHelper(@NonNull final Handler handler) { 1088 mHandler = handler; 1089 } 1090 1091 /** 1092 * Posts {@code r} to {@code handler} with a delay of {@code delayMillis} 1093 * 1094 * @param r the runnable callback 1095 * @param delayMillis the number of milliseconds to delay 1096 */ postDelayed(Runnable r, long delayMillis)1097 public void postDelayed(Runnable r, long delayMillis) { 1098 mHandler.postDelayed(r, delayMillis); 1099 } 1100 1101 /** 1102 * Posts {@code r} to the underlying handler 1103 * 1104 * @param r the runnable callback 1105 */ post(Runnable r)1106 public void post(Runnable r) { 1107 mHandler.post(r); 1108 } 1109 1110 /** 1111 * Gets the underlying handler 1112 * @return the handler 1113 */ getHandler()1114 public Handler getHandler() { 1115 return mHandler; 1116 } 1117 1118 /** 1119 * Remove any pending posts of Runnable r that are in the message queue. 1120 */ removeCallbacks(Runnable r)1121 public void removeCallbacks(Runnable r) { 1122 mHandler.removeCallbacks(r); 1123 } 1124 } 1125 } 1126