1 /* 2 * Copyright 2014, 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.server.telecom; 18 19 import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED; 20 import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.database.Cursor; 28 import android.location.Country; 29 import android.location.CountryDetector; 30 import android.location.Location; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.os.Looper; 34 import android.os.UserHandle; 35 import android.os.PersistableBundle; 36 import android.os.UserManager; 37 import android.provider.CallLog; 38 import android.provider.CallLog.Calls; 39 import android.telecom.Connection; 40 import android.telecom.DisconnectCause; 41 import android.telecom.Log; 42 import android.telecom.PhoneAccount; 43 import android.telecom.PhoneAccountHandle; 44 import android.telecom.TelecomManager; 45 import android.telecom.VideoProfile; 46 import android.telephony.CarrierConfigManager; 47 import android.telephony.PhoneNumberUtils; 48 import android.telephony.SubscriptionManager; 49 import android.util.Pair; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.server.telecom.callfiltering.CallFilteringResult; 53 54 import java.util.Arrays; 55 import java.util.Locale; 56 import java.util.Objects; 57 import java.util.UUID; 58 import java.util.stream.Stream; 59 60 /** 61 * Helper class that provides functionality to write information about calls and their associated 62 * caller details to the call log. All logging activity will be performed asynchronously in a 63 * background thread to avoid blocking on the main thread. 64 */ 65 @VisibleForTesting 66 public final class CallLogManager extends CallsManagerListenerBase { 67 68 public interface LogCallCompletedListener { onLogCompleted(@ullable Uri uri)69 void onLogCompleted(@Nullable Uri uri); 70 } 71 72 /** 73 * Parameter object to hold the arguments to add a call in the call log DB. 74 */ 75 private static class AddCallArgs { AddCallArgs(Context context, CallLog.AddCallParams params, @Nullable LogCallCompletedListener logCallCompletedListener, @NonNull String callId)76 public AddCallArgs(Context context, CallLog.AddCallParams params, 77 @Nullable LogCallCompletedListener logCallCompletedListener, 78 @NonNull String callId) { 79 this.context = context; 80 this.params = params; 81 this.logCallCompletedListener = logCallCompletedListener; 82 this.callId = callId; 83 84 } 85 // Since the members are accessed directly, we don't use the 86 // mXxxx notation. 87 public final Context context; 88 public final CallLog.AddCallParams params; 89 public final String callId; 90 @Nullable 91 public final LogCallCompletedListener logCallCompletedListener; 92 } 93 94 private static final String TAG = CallLogManager.class.getSimpleName(); 95 96 // Copied from android.telephony.DisconnectCause.toString 97 // TODO: come up with a better way to indicate in a android.telecom.DisconnectCause that 98 // a conference was merged successfully 99 private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY"; 100 private static final UUID LOG_CALL_FAILED_ANOMALY_ID = 101 UUID.fromString("1c4c15f3-ab4f-459c-b9ef-43d2988bae82"); 102 private static final String LOG_CALL_FAILED_ANOMALY_DESC = 103 "Failed to record a call to the call log."; 104 105 private final Context mContext; 106 private final CarrierConfigManager mCarrierConfigManager; 107 private final PhoneAccountRegistrar mPhoneAccountRegistrar; 108 private final MissedCallNotifier mMissedCallNotifier; 109 private AnomalyReporterAdapter mAnomalyReporterAdapter; 110 private static final String ACTION_CALLS_TABLE_ADD_ENTRY = 111 "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY"; 112 private static final String PERMISSION_PROCESS_CALLLOG_INFO = 113 "android.permission.PROCESS_CALLLOG_INFO"; 114 private static final String CALL_TYPE = "callType"; 115 private static final String CALL_DURATION = "duration"; 116 117 private Object mLock; 118 private String mCurrentCountryIso; 119 CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar, MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter)120 public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar, 121 MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter) { 122 mContext = context; 123 mCarrierConfigManager = (CarrierConfigManager) mContext 124 .getSystemService(Context.CARRIER_CONFIG_SERVICE); 125 mPhoneAccountRegistrar = phoneAccountRegistrar; 126 mMissedCallNotifier = missedCallNotifier; 127 mAnomalyReporterAdapter = anomalyReporterAdapter; 128 mLock = new Object(); 129 } 130 131 @Override onCallStateChanged(Call call, int oldState, int newState)132 public void onCallStateChanged(Call call, int oldState, int newState) { 133 int disconnectCause = call.getDisconnectCause().getCode(); 134 boolean isNewlyDisconnected = 135 newState == CallState.DISCONNECTED || newState == CallState.ABORTED; 136 boolean isCallCanceled = isNewlyDisconnected && disconnectCause == DisconnectCause.CANCELED; 137 138 if (!isNewlyDisconnected) { 139 return; 140 } 141 142 if (shouldLogDisconnectedCall(call, oldState, isCallCanceled)) { 143 int type; 144 if (!call.isIncoming()) { 145 type = Calls.OUTGOING_TYPE; 146 } else if (disconnectCause == DisconnectCause.MISSED) { 147 type = Calls.MISSED_TYPE; 148 } else if (disconnectCause == DisconnectCause.ANSWERED_ELSEWHERE) { 149 type = Calls.ANSWERED_EXTERNALLY_TYPE; 150 } else if (disconnectCause == DisconnectCause.REJECTED) { 151 type = Calls.REJECTED_TYPE; 152 } else { 153 type = Calls.INCOMING_TYPE; 154 } 155 // Always show the notification for managed calls. For self-managed calls, it is up to 156 // the app to show the notification, so suppress the notification when logging the call. 157 boolean showNotification = !call.isSelfManaged(); 158 logCall(call, type, showNotification, null /*result*/); 159 } 160 } 161 162 /** 163 * Log newly disconnected calls only if all of below conditions are met: 164 * Call was NOT in the "choose account" phase when disconnected 165 * Call is NOT a conference call which had children (unless it was remotely hosted). 166 * Call is NOT a child call from a conference which was remotely hosted. 167 * Call is NOT simulating a single party conference. 168 * Call was NOT explicitly canceled, except for disconnecting from a conference. 169 * Call is NOT an external call 170 * Call is NOT disconnected because of merging into a conference. 171 * Call is NOT a self-managed call OR call is a self-managed call which has indicated it 172 * should be logged in its PhoneAccount 173 */ 174 @VisibleForTesting shouldLogDisconnectedCall(Call call, int oldState, boolean isCallCanceled)175 public boolean shouldLogDisconnectedCall(Call call, int oldState, boolean isCallCanceled) { 176 boolean shouldCallSelfManagedLogged = call.isLoggedSelfManaged() 177 && (call.getHandoverState() == HandoverState.HANDOVER_NONE 178 || call.getHandoverState() == HandoverState.HANDOVER_COMPLETE); 179 180 // "Choose account" phase when disconnected 181 if (oldState == CallState.SELECT_PHONE_ACCOUNT) { 182 return false; 183 } 184 // A conference call which had children should not be logged, unless it was remotely hosted. 185 if (call.isConference() && call.hadChildren() && 186 !call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) { 187 return false; 188 } 189 190 // A conference call which had no children should not be logged; this case will occur on IMS 191 // when no conference event package data is received. We will have logged the participants 192 // as they merge into the conference, so we should not log the conference itself. 193 if (call.isConference() && !call.hadChildren() && 194 !call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) { 195 return false; 196 } 197 198 // A child call of a conference which was remotely hosted; these didn't originate on this 199 // device and should not be logged. 200 if (call.getParentCall() != null && call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) { 201 return false; 202 } 203 204 DisconnectCause cause = call.getDisconnectCause(); 205 if (isCallCanceled) { 206 // No log when disconnecting to simulate a single party conference. 207 if (cause != null 208 && DisconnectCause.REASON_EMULATING_SINGLE_CALL.equals(cause.getReason())) { 209 return false; 210 } 211 // Explicitly canceled 212 // Conference children connections only have CAPABILITY_DISCONNECT_FROM_CONFERENCE. 213 // Log them when they are disconnected from conference. 214 return (call.getConnectionCapabilities() 215 & Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE) 216 == Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE; 217 } 218 // An external call 219 if (call.isExternalCall()) { 220 return false; 221 } 222 223 // Call merged into conferences and marked with IMS_MERGED_SUCCESSFULLY. 224 // Return false if the conference supports the participants packets for the carrier. 225 // Otherwise, fall through. Merged calls would be associated with disconnected 226 // connections because of special carrier requirements. Those calls don't look like 227 // merged, e.g. could be one active and the other on hold. 228 if (cause != null && REASON_IMS_MERGED_SUCCESSFULLY.equals(cause.getReason())) { 229 int subscriptionId = mPhoneAccountRegistrar 230 .getSubscriptionIdForPhoneAccount(call.getTargetPhoneAccount()); 231 // By default, the conference should return a list of participants. 232 if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 233 return false; 234 } 235 236 PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId); 237 if (b == null) { 238 return false; 239 } 240 241 if (b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) { 242 return false; 243 } 244 } 245 246 // Call is NOT a self-managed call OR call is a self-managed call which has indicated it 247 // should be logged in its PhoneAccount 248 return !call.isSelfManaged() || shouldCallSelfManagedLogged; 249 } 250 logCall(Call call, int type, boolean showNotificationForMissedCall, CallFilteringResult result)251 void logCall(Call call, int type, boolean showNotificationForMissedCall, CallFilteringResult 252 result) { 253 if ((type == Calls.MISSED_TYPE || type == Calls.BLOCKED_TYPE) && 254 showNotificationForMissedCall) { 255 logCall(call, type, new LogCallCompletedListener() { 256 @Override 257 public void onLogCompleted(@Nullable Uri uri) { 258 mMissedCallNotifier.showMissedCallNotification( 259 new MissedCallNotifier.CallInfo(call)); 260 } 261 }, result); 262 } else { 263 logCall(call, type, null, result); 264 } 265 } 266 267 /** 268 * Logs a call to the call log based on the {@link Call} object passed in. 269 * 270 * @param call The call object being logged 271 * @param callLogType The type of call log entry to log this call as. See: 272 * {@link android.provider.CallLog.Calls#INCOMING_TYPE} 273 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE} 274 * {@link android.provider.CallLog.Calls#MISSED_TYPE} 275 * {@link android.provider.CallLog.Calls#BLOCKED_TYPE} 276 * @param logCallCompletedListener optional callback called after the call is logged. 277 * @param result is generated when call type is 278 * {@link android.provider.CallLog.Calls#BLOCKED_TYPE}. 279 */ logCall(Call call, int callLogType, @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result)280 void logCall(Call call, int callLogType, 281 @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) { 282 283 CallLog.AddCallParams.AddCallParametersBuilder paramBuilder = 284 new CallLog.AddCallParams.AddCallParametersBuilder(); 285 if (call.getConnectTimeMillis() != 0 286 && call.getConnectTimeMillis() < call.getCreationTimeMillis()) { 287 // If connected time is available, use connected time. The connected time might be 288 // earlier than created time since it might come from carrier sent special SMS to 289 // notifier user earlier missed call. 290 paramBuilder.setStart(call.getConnectTimeMillis()); 291 } else { 292 paramBuilder.setStart(call.getCreationTimeMillis()); 293 } 294 295 paramBuilder.setDuration((int) (call.getAgeMillis() / 1000)); 296 297 String logNumber = getLogNumber(call); 298 paramBuilder.setNumber(logNumber); 299 300 Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber)); 301 302 String formattedViaNumber = PhoneNumberUtils.formatNumber(call.getViaNumber(), 303 getCountryIso()); 304 formattedViaNumber = (formattedViaNumber != null) ? 305 formattedViaNumber : call.getViaNumber(); 306 paramBuilder.setViaNumber(formattedViaNumber); 307 308 final PhoneAccountHandle emergencyAccountHandle = 309 TelephonyUtil.getDefaultEmergencyPhoneAccount().getAccountHandle(); 310 PhoneAccountHandle accountHandle = call.getTargetPhoneAccount(); 311 if (emergencyAccountHandle.equals(accountHandle)) { 312 accountHandle = null; 313 } 314 paramBuilder.setAccountHandle(accountHandle); 315 316 paramBuilder.setDataUsage(call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET 317 ? Long.MIN_VALUE : call.getCallDataUsage()); 318 319 paramBuilder.setFeatures(getCallFeatures(call.getVideoStateHistory(), 320 call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED, 321 call.wasHighDefAudio(), call.wasWifi(), 322 (call.getConnectionProperties() & Connection.PROPERTY_ASSISTED_DIALING) == 323 Connection.PROPERTY_ASSISTED_DIALING, 324 call.wasEverRttCall(), 325 call.wasVolte())); 326 327 if (result == null) { 328 result = new CallFilteringResult.Builder() 329 .setCallScreeningAppName(call.getCallScreeningAppName()) 330 .setCallScreeningComponentName(call.getCallScreeningComponentName()) 331 .build(); 332 } 333 if (callLogType == Calls.BLOCKED_TYPE || callLogType == Calls.MISSED_TYPE) { 334 paramBuilder.setCallBlockReason(result.mCallBlockReason); 335 paramBuilder.setCallScreeningComponentName(result.mCallScreeningComponentName); 336 paramBuilder.setCallScreeningAppName(result.mCallScreeningAppName); 337 } else { 338 paramBuilder.setCallBlockReason(BLOCK_REASON_NOT_BLOCKED); 339 } 340 341 PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(accountHandle); 342 UserHandle initiatingUser = call.getAssociatedUser(); 343 if (phoneAccount != null && 344 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) { 345 if (initiatingUser != null && 346 UserUtil.isManagedProfile(mContext, initiatingUser)) { 347 paramBuilder.setUserToBeInsertedTo(initiatingUser); 348 paramBuilder.setAddForAllUsers(false); 349 } else { 350 paramBuilder.setAddForAllUsers(true); 351 } 352 } else { 353 if (accountHandle == null) { 354 paramBuilder.setAddForAllUsers(true); 355 } else { 356 paramBuilder.setUserToBeInsertedTo(accountHandle.getUserHandle()); 357 paramBuilder.setAddForAllUsers(accountHandle.getUserHandle() == null); 358 } 359 } 360 if (call.getIntentExtras() != null) { 361 if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_PRIORITY)) { 362 paramBuilder.setPriority(call.getIntentExtras() 363 .getInt(TelecomManager.EXTRA_PRIORITY)); 364 } 365 if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_CALL_SUBJECT)) { 366 paramBuilder.setSubject(call.getIntentExtras() 367 .getString(TelecomManager.EXTRA_CALL_SUBJECT)); 368 } 369 if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_PICTURE_URI)) { 370 paramBuilder.setPictureUri(call.getIntentExtras() 371 .getParcelable(TelecomManager.EXTRA_PICTURE_URI)); 372 } 373 // The picture uri can end up either in extras or in intent extras due to how these 374 // two bundles are set. For incoming calls they're in extras, but for outgoing calls 375 // they're in intentExtras. 376 if (call.getExtras() != null 377 && call.getExtras().containsKey(TelecomManager.EXTRA_PICTURE_URI)) { 378 paramBuilder.setPictureUri(call.getExtras() 379 .getParcelable(TelecomManager.EXTRA_PICTURE_URI)); 380 } 381 if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_LOCATION)) { 382 Location l = call.getIntentExtras().getParcelable(TelecomManager.EXTRA_LOCATION); 383 if (l != null) { 384 paramBuilder.setLatitude(l.getLatitude()); 385 paramBuilder.setLongitude(l.getLongitude()); 386 } 387 } 388 } 389 390 paramBuilder.setCallerInfo(call.getCallerInfo()); 391 paramBuilder.setPostDialDigits(call.getPostDialDigits()); 392 paramBuilder.setPresentation(call.getHandlePresentation()); 393 paramBuilder.setCallType(callLogType); 394 paramBuilder.setIsRead(call.isSelfManaged()); 395 paramBuilder.setMissedReason(call.getMissedReason()); 396 397 sendAddCallBroadcast(callLogType, call.getAgeMillis()); 398 399 boolean okayToLog = 400 okayToLogCall(accountHandle, logNumber, call.isEmergencyCall()); 401 if (okayToLog) { 402 AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(), 403 logCallCompletedListener, call.getId()); 404 Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber) 405 + ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres=" 406 + call.getHandlePresentation()); 407 logCallAsync(args); 408 } else { 409 Log.addEvent(call, LogUtils.Events.SKIP_CALL_LOG); 410 } 411 } 412 okayToLogCall(PhoneAccountHandle accountHandle, String number, boolean isEmergency)413 boolean okayToLogCall(PhoneAccountHandle accountHandle, String number, boolean isEmergency) { 414 // On some devices, to avoid accidental redialing of emergency numbers, we *never* log 415 // emergency calls to the Call Log. (This behavior is set on a per-product basis, based 416 // on carrier requirements.) 417 boolean okToLogEmergencyNumber = false; 418 CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService( 419 Context.CARRIER_CONFIG_SERVICE); 420 PersistableBundle configBundle = configManager.getConfigForSubId( 421 mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)); 422 if (configBundle != null) { 423 okToLogEmergencyNumber = configBundle.getBoolean( 424 CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL); 425 } 426 427 // Don't log emergency numbers if the device doesn't allow it. 428 return (!isEmergency || okToLogEmergencyNumber) 429 && !isUnloggableNumber(number, configBundle); 430 } 431 isUnloggableNumber(String callNumber, PersistableBundle carrierConfig)432 private boolean isUnloggableNumber(String callNumber, PersistableBundle carrierConfig) { 433 String normalizedNumber = PhoneNumberUtils.normalizeNumber(callNumber); 434 String[] unloggableNumbersFromCarrierConfig = carrierConfig == null ? null 435 : carrierConfig.getStringArray( 436 CarrierConfigManager.KEY_UNLOGGABLE_NUMBERS_STRING_ARRAY); 437 String[] unloggableNumbersFromMccConfig = mContext.getResources() 438 .getStringArray(com.android.internal.R.array.unloggable_phone_numbers); 439 return Stream.concat( 440 unloggableNumbersFromCarrierConfig == null ? 441 Stream.empty() : Arrays.stream(unloggableNumbersFromCarrierConfig), 442 unloggableNumbersFromMccConfig == null ? 443 Stream.empty() : Arrays.stream(unloggableNumbersFromMccConfig) 444 ).anyMatch(unloggableNumber -> Objects.equals(unloggableNumber, normalizedNumber)); 445 } 446 447 /** 448 * Based on the video state of the call, determines the call features applicable for the call. 449 * 450 * @param videoState The video state. 451 * @param isPulledCall {@code true} if this call was pulled to another device. 452 * @param isStoreHd {@code true} if this call was used HD. 453 * @param isWifi {@code true} if this call was used wifi. 454 * @param isUsingAssistedDialing {@code true} if this call used assisted dialing. 455 * @return The call features. 456 */ getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd, boolean isWifi, boolean isUsingAssistedDialing, boolean isRtt, boolean isVolte)457 private static int getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd, 458 boolean isWifi, boolean isUsingAssistedDialing, boolean isRtt, boolean isVolte) { 459 int features = 0; 460 if (VideoProfile.isVideo(videoState)) { 461 features |= Calls.FEATURES_VIDEO; 462 } 463 if (isPulledCall) { 464 features |= Calls.FEATURES_PULLED_EXTERNALLY; 465 } 466 if (isStoreHd) { 467 features |= Calls.FEATURES_HD_CALL; 468 } 469 if (isWifi) { 470 features |= Calls.FEATURES_WIFI; 471 } 472 if (isUsingAssistedDialing) { 473 features |= Calls.FEATURES_ASSISTED_DIALING_USED; 474 } 475 if (isRtt) { 476 features |= Calls.FEATURES_RTT; 477 } 478 if (isVolte) { 479 features |= Calls.FEATURES_VOLTE; 480 } 481 return features; 482 } 483 484 /** 485 * Retrieve the phone number from the call, and then process it before returning the 486 * actual number that is to be logged. 487 * 488 * @param call The phone connection. 489 * @return the phone number to be logged. 490 */ getLogNumber(Call call)491 private String getLogNumber(Call call) { 492 Uri handle = call.getOriginalHandle(); 493 494 if (handle == null) { 495 return null; 496 } 497 498 String handleString = handle.getSchemeSpecificPart(); 499 if (!PhoneNumberUtils.isUriNumber(handleString)) { 500 handleString = PhoneNumberUtils.stripSeparators(handleString); 501 } 502 return handleString; 503 } 504 505 /** 506 * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider 507 * using an AsyncTask to avoid blocking the main thread. 508 * 509 * @param args Prepopulated call details. 510 * @return A handle to the AsyncTask that will add the call to the call log asynchronously. 511 */ logCallAsync(AddCallArgs args)512 public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) { 513 return new LogCallAsyncTask().execute(args); 514 } 515 516 /** 517 * Helper AsyncTask to access the call logs database asynchronously since database operations 518 * can take a long time depending on the system's load. Since it extends AsyncTask, it uses 519 * its own thread pool. 520 */ 521 private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> { 522 523 private LogCallCompletedListener[] mListeners; 524 525 @Override doInBackground(AddCallArgs... callList)526 protected Uri[] doInBackground(AddCallArgs... callList) { 527 int count = callList.length; 528 Uri[] result = new Uri[count]; 529 mListeners = new LogCallCompletedListener[count]; 530 for (int i = 0; i < count; i++) { 531 AddCallArgs c = callList[i]; 532 mListeners[i] = c.logCallCompletedListener; 533 try { 534 // May block. 535 ContentResolver resolver = c.context.getContentResolver(); 536 Pair<Integer, Integer> startStats = getCallLogStats(resolver); 537 Log.i(TAG, "LogCall; about to log callId=%s, " 538 + "startCount=%d, startMaxId=%d", 539 c.callId, startStats.first, startStats.second); 540 541 result[i] = Calls.addCall(c.context, c.params); 542 Pair<Integer, Integer> endStats = getCallLogStats(resolver); 543 Log.i(TAG, "LogCall; logged callId=%s, uri=%s, " 544 + "endCount=%d, endMaxId=%s", 545 c.callId, result, endStats.first, endStats.second); 546 if ((endStats.second - startStats.second) <= 0) { 547 // No call was added or even worse we lost a call in the log. Trigger an 548 // anomaly report. Note: it technically possible that an app modified the 549 // call log while we were writing to it here; that is pretty unlikely, and 550 // the goal here is to try and identify potential anomalous conditions with 551 // logging calls. 552 mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID, 553 LOG_CALL_FAILED_ANOMALY_DESC); 554 } 555 } catch (Exception e) { 556 // This is very rare but may happen in legitimate cases. 557 // E.g. If the phone is encrypted and thus write request fails, it may cause 558 // some kind of Exception (right now it is IllegalArgumentException, but this 559 // might change). 560 // 561 // We don't want to crash the whole process just because of that, so just log 562 // it instead. 563 Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.callId); 564 result[i] = null; 565 mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID, 566 LOG_CALL_FAILED_ANOMALY_DESC); 567 } 568 } 569 return result; 570 } 571 572 @Override onPostExecute(Uri[] result)573 protected void onPostExecute(Uri[] result) { 574 for (int i = 0; i < result.length; i++) { 575 Uri uri = result[i]; 576 /* 577 Performs a simple correctness check to make sure the call was written in the 578 database. 579 Typically there is only one result per call so it is easy to identify which one 580 failed. 581 */ 582 if (uri == null) { 583 Log.w(TAG, "Failed to write call to the log."); 584 } 585 if (mListeners[i] != null) { 586 mListeners[i].onLogCompleted(uri); 587 } 588 } 589 } 590 } 591 sendAddCallBroadcast(int callType, long duration)592 private void sendAddCallBroadcast(int callType, long duration) { 593 Intent callAddIntent = new Intent(ACTION_CALLS_TABLE_ADD_ENTRY); 594 callAddIntent.putExtra(CALL_TYPE, callType); 595 callAddIntent.putExtra(CALL_DURATION, duration); 596 mContext.sendBroadcast(callAddIntent, PERMISSION_PROCESS_CALLLOG_INFO); 597 } 598 getCountryIsoFromCountry(Country country)599 private String getCountryIsoFromCountry(Country country) { 600 if(country == null) { 601 // Fallback to Locale if there are issues with CountryDetector 602 Log.w(TAG, "Value for country was null. Falling back to Locale."); 603 return Locale.getDefault().getCountry(); 604 } 605 606 return country.getCountryIso(); 607 } 608 609 /** 610 * Get the current country code 611 * 612 * @return the ISO 3166-1 two letters country code of current country. 613 */ getCountryIso()614 public String getCountryIso() { 615 synchronized (mLock) { 616 if (mCurrentCountryIso == null) { 617 Log.i(TAG, "Country cache is null. Detecting Country and Setting Cache..."); 618 final CountryDetector countryDetector = 619 (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR); 620 Country country = null; 621 if (countryDetector != null) { 622 country = countryDetector.detectCountry(); 623 624 countryDetector.addCountryListener((newCountry) -> { 625 Log.startSession("CLM.oCD"); 626 try { 627 synchronized (mLock) { 628 Log.i(TAG, "Country ISO changed. Retrieving new ISO..."); 629 mCurrentCountryIso = getCountryIsoFromCountry(newCountry); 630 } 631 } finally { 632 Log.endSession(); 633 } 634 }, Looper.getMainLooper()); 635 } 636 mCurrentCountryIso = getCountryIsoFromCountry(country); 637 } 638 return mCurrentCountryIso; 639 } 640 } 641 642 643 /** 644 * Returns a pair containing the number of rows in the call log, as well as the maximum call log 645 * ID. There is a limit of 500 entries in the call log for a phone account, so once we hit 500 646 * we can reasonably expect that number to not change before and after logging a call. 647 * We determine the maximum ID in the call log since this is a way we can objectively check if 648 * the provider did record a call log entry or not. Ideally there should me more call log 649 * entries after logging than before, and certainly not less. 650 * @param resolver content resolver 651 * @return pair with number of rows in the call log and max id. 652 */ getCallLogStats(@onNull ContentResolver resolver)653 private Pair<Integer, Integer> getCallLogStats(@NonNull ContentResolver resolver) { 654 try { 655 final UserManager userManager = mContext.getSystemService(UserManager.class); 656 final int currentUserId = userManager.getProcessUserId(); 657 658 // Use shadow provider based on current user unlock state. 659 Uri providerUri; 660 if (userManager.isUserUnlocked(currentUserId)) { 661 providerUri = Calls.CONTENT_URI; 662 } else { 663 providerUri = Calls.SHADOW_CONTENT_URI; 664 } 665 int maxCallId = -1; 666 int numFound; 667 Cursor countCursor = resolver.query(providerUri, 668 new String[]{Calls._ID}, 669 null, 670 null, 671 Calls._ID + " DESC"); 672 try { 673 numFound = countCursor.getCount(); 674 if (numFound > 0) { 675 countCursor.moveToFirst(); 676 maxCallId = countCursor.getInt(0); 677 } 678 } finally { 679 countCursor.close(); 680 } 681 return new Pair<>(numFound, maxCallId); 682 } catch (Exception e) { 683 // Oh jeepers, we crashed getting the call count. 684 Log.e(TAG, e, "getCountOfCallLogRows: failed"); 685 return new Pair<>(-1, -1); 686 } 687 } 688 689 @VisibleForTesting setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter)690 public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){ 691 mAnomalyReporterAdapter = anomalyReporterAdapter; 692 } 693 } 694