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