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