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 android.annotation.Nullable; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.location.Country; 23 import android.location.CountryDetector; 24 import android.net.Uri; 25 import android.os.AsyncTask; 26 import android.os.Looper; 27 import android.os.UserHandle; 28 import android.os.PersistableBundle; 29 import android.provider.CallLog.Calls; 30 import android.telecom.Connection; 31 import android.telecom.DisconnectCause; 32 import android.telecom.Log; 33 import android.telecom.PhoneAccount; 34 import android.telecom.PhoneAccountHandle; 35 import android.telecom.VideoProfile; 36 import android.telephony.CarrierConfigManager; 37 import android.telephony.PhoneNumberUtils; 38 39 // TODO: Needed for move to system service: import com.android.internal.R; 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.telephony.CallerInfo; 42 43 import java.util.Locale; 44 45 /** 46 * Helper class that provides functionality to write information about calls and their associated 47 * caller details to the call log. All logging activity will be performed asynchronously in a 48 * background thread to avoid blocking on the main thread. 49 */ 50 @VisibleForTesting 51 public final class CallLogManager extends CallsManagerListenerBase { 52 53 public interface LogCallCompletedListener { 54 void onLogCompleted(@Nullable Uri uri); 55 } 56 57 /** 58 * Parameter object to hold the arguments to add a call in the call log DB. 59 */ 60 private static class AddCallArgs { 61 /** 62 * @param callerInfo Caller details. 63 * @param number The phone number to be logged. 64 * @param presentation Number presentation of the phone number to be logged. 65 * @param callType The type of call (e.g INCOMING_TYPE). @see 66 * {@link android.provider.CallLog} for the list of values. 67 * @param features The features of the call (e.g. FEATURES_VIDEO). @see 68 * {@link android.provider.CallLog} for the list of values. 69 * @param creationDate Time when the call was created (milliseconds since epoch). 70 * @param durationInMillis Duration of the call (milliseconds). 71 * @param dataUsage Data usage in bytes, or null if not applicable. 72 * @param isRead Indicates if the entry has been read or not. 73 * @param logCallCompletedListener optional callback called after the call is logged. 74 */ 75 public AddCallArgs(Context context, CallerInfo callerInfo, String number, 76 String postDialDigits, String viaNumber, int presentation, int callType, 77 int features, PhoneAccountHandle accountHandle, long creationDate, 78 long durationInMillis, Long dataUsage, UserHandle initiatingUser, boolean isRead, 79 @Nullable LogCallCompletedListener logCallCompletedListener) { 80 this.context = context; 81 this.callerInfo = callerInfo; 82 this.number = number; 83 this.postDialDigits = postDialDigits; 84 this.viaNumber = viaNumber; 85 this.presentation = presentation; 86 this.callType = callType; 87 this.features = features; 88 this.accountHandle = accountHandle; 89 this.timestamp = creationDate; 90 this.durationInSec = (int)(durationInMillis / 1000); 91 this.dataUsage = dataUsage; 92 this.initiatingUser = initiatingUser; 93 this.isRead = isRead; 94 this.logCallCompletedListener = logCallCompletedListener; 95 } 96 // Since the members are accessed directly, we don't use the 97 // mXxxx notation. 98 public final Context context; 99 public final CallerInfo callerInfo; 100 public final String number; 101 public final String postDialDigits; 102 public final String viaNumber; 103 public final int presentation; 104 public final int callType; 105 public final int features; 106 public final PhoneAccountHandle accountHandle; 107 public final long timestamp; 108 public final int durationInSec; 109 public final Long dataUsage; 110 public final UserHandle initiatingUser; 111 public final boolean isRead; 112 113 @Nullable 114 public final LogCallCompletedListener logCallCompletedListener; 115 } 116 117 private static final String TAG = CallLogManager.class.getSimpleName(); 118 119 private final Context mContext; 120 private final PhoneAccountRegistrar mPhoneAccountRegistrar; 121 private final MissedCallNotifier mMissedCallNotifier; 122 private static final String ACTION_CALLS_TABLE_ADD_ENTRY = 123 "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY"; 124 private static final String PERMISSION_PROCESS_CALLLOG_INFO = 125 "android.permission.PROCESS_CALLLOG_INFO"; 126 private static final String CALL_TYPE = "callType"; 127 private static final String CALL_DURATION = "duration"; 128 129 private Object mLock; 130 private String mCurrentCountryIso; 131 132 public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar, 133 MissedCallNotifier missedCallNotifier) { 134 mContext = context; 135 mPhoneAccountRegistrar = phoneAccountRegistrar; 136 mMissedCallNotifier = missedCallNotifier; 137 mLock = new Object(); 138 } 139 140 @Override 141 public void onCallStateChanged(Call call, int oldState, int newState) { 142 int disconnectCause = call.getDisconnectCause().getCode(); 143 boolean isNewlyDisconnected = 144 newState == CallState.DISCONNECTED || newState == CallState.ABORTED; 145 boolean isCallCanceled = isNewlyDisconnected && disconnectCause == DisconnectCause.CANCELED; 146 147 // Log newly disconnected calls only if: 148 // 1) It was not in the "choose account" phase when disconnected 149 // 2) It is a conference call 150 // 3) Call was not explicitly canceled 151 // 4) Call is not an external call 152 // 5) Call is not a self-managed call OR call is a self-managed call which has indicated it 153 // should be logged in its PhoneAccount 154 if (isNewlyDisconnected && 155 (oldState != CallState.SELECT_PHONE_ACCOUNT && 156 !call.isConference() && 157 !isCallCanceled) && 158 !call.isExternalCall() && 159 (!call.isSelfManaged() || 160 (call.isLoggedSelfManaged() && 161 (call.getHandoverState() == HandoverState.HANDOVER_NONE || 162 call.getHandoverState() == HandoverState.HANDOVER_COMPLETE)))) { 163 int type; 164 if (!call.isIncoming()) { 165 type = Calls.OUTGOING_TYPE; 166 } else if (disconnectCause == DisconnectCause.MISSED) { 167 type = Calls.MISSED_TYPE; 168 } else if (disconnectCause == DisconnectCause.ANSWERED_ELSEWHERE) { 169 type = Calls.ANSWERED_EXTERNALLY_TYPE; 170 } else if (disconnectCause == DisconnectCause.REJECTED) { 171 type = Calls.REJECTED_TYPE; 172 } else { 173 type = Calls.INCOMING_TYPE; 174 } 175 // Always show the notification for managed calls. For self-managed calls, it is up to 176 // the app to show the notification, so suppress the notification when logging the call. 177 boolean showNotification = !call.isSelfManaged(); 178 logCall(call, type, showNotification); 179 } 180 } 181 182 void logCall(Call call, int type, boolean showNotificationForMissedCall) { 183 if (type == Calls.MISSED_TYPE && showNotificationForMissedCall) { 184 logCall(call, Calls.MISSED_TYPE, 185 new LogCallCompletedListener() { 186 @Override 187 public void onLogCompleted(@Nullable Uri uri) { 188 mMissedCallNotifier.showMissedCallNotification( 189 new MissedCallNotifier.CallInfo(call)); 190 } 191 }); 192 } else { 193 logCall(call, type, null); 194 } 195 } 196 197 /** 198 * Logs a call to the call log based on the {@link Call} object passed in. 199 * 200 * @param call The call object being logged 201 * @param callLogType The type of call log entry to log this call as. See: 202 * {@link android.provider.CallLog.Calls#INCOMING_TYPE} 203 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE} 204 * {@link android.provider.CallLog.Calls#MISSED_TYPE} 205 * @param logCallCompletedListener optional callback called after the call is logged. 206 */ 207 void logCall(Call call, int callLogType, 208 @Nullable LogCallCompletedListener logCallCompletedListener) { 209 final long creationTime = call.getCreationTimeMillis(); 210 final long age = call.getAgeMillis(); 211 212 final String logNumber = getLogNumber(call); 213 214 Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber)); 215 216 final PhoneAccountHandle emergencyAccountHandle = 217 TelephonyUtil.getDefaultEmergencyPhoneAccount().getAccountHandle(); 218 219 String formattedViaNumber = PhoneNumberUtils.formatNumber(call.getViaNumber(), 220 getCountryIso()); 221 formattedViaNumber = (formattedViaNumber != null) ? 222 formattedViaNumber : call.getViaNumber(); 223 224 PhoneAccountHandle accountHandle = call.getTargetPhoneAccount(); 225 if (emergencyAccountHandle.equals(accountHandle)) { 226 accountHandle = null; 227 } 228 229 Long callDataUsage = call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET ? null : 230 call.getCallDataUsage(); 231 232 int callFeatures = getCallFeatures(call.getVideoStateHistory(), 233 call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED, 234 shouldSaveHdInfo(call, accountHandle), 235 (call.getConnectionProperties() & Connection.PROPERTY_ASSISTED_DIALING_USED) == 236 Connection.PROPERTY_ASSISTED_DIALING_USED, 237 call.wasEverRttCall()); 238 logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(), formattedViaNumber, 239 call.getHandlePresentation(), callLogType, callFeatures, accountHandle, 240 creationTime, age, callDataUsage, call.isEmergencyCall(), call.getInitiatingUser(), 241 call.isSelfManaged(), logCallCompletedListener); 242 } 243 244 /** 245 * Inserts a call into the call log, based on the parameters passed in. 246 * 247 * @param callerInfo Caller details. 248 * @param number The number the call was made to or from. 249 * @param postDialDigits The post-dial digits that were dialed after the number, 250 * if it was an outgoing call. Otherwise ''. 251 * @param presentation 252 * @param callType The type of call. 253 * @param features The features of the call. 254 * @param start The start time of the call, in milliseconds. 255 * @param duration The duration of the call, in milliseconds. 256 * @param dataUsage The data usage for the call, null if not applicable. 257 * @param isEmergency {@code true} if this is an emergency call, {@code false} otherwise. 258 * @param logCallCompletedListener optional callback called after the call is logged. 259 * @param initiatingUser The user the call was initiated under. 260 * @param isSelfManaged {@code true} if this is a self-managed call, {@code false} otherwise. 261 */ 262 private void logCall( 263 CallerInfo callerInfo, 264 String number, 265 String postDialDigits, 266 String viaNumber, 267 int presentation, 268 int callType, 269 int features, 270 PhoneAccountHandle accountHandle, 271 long start, 272 long duration, 273 Long dataUsage, 274 boolean isEmergency, 275 UserHandle initiatingUser, 276 boolean isSelfManaged, 277 @Nullable LogCallCompletedListener logCallCompletedListener) { 278 279 // On some devices, to avoid accidental redialing of emergency numbers, we *never* log 280 // emergency calls to the Call Log. (This behavior is set on a per-product basis, based 281 // on carrier requirements.) 282 boolean okToLogEmergencyNumber = false; 283 CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService( 284 Context.CARRIER_CONFIG_SERVICE); 285 PersistableBundle configBundle = configManager.getConfigForSubId( 286 mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)); 287 if (configBundle != null) { 288 okToLogEmergencyNumber = configBundle.getBoolean( 289 CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL); 290 } 291 292 // Don't log emergency numbers if the device doesn't allow it. 293 final boolean isOkToLogThisCall = !isEmergency || okToLogEmergencyNumber; 294 295 sendAddCallBroadcast(callType, duration); 296 297 if (isOkToLogThisCall) { 298 Log.d(TAG, "Logging Call log entry: " + callerInfo + ", " 299 + Log.pii(number) + "," + presentation + ", " + callType 300 + ", " + start + ", " + duration); 301 boolean isRead = false; 302 if (isSelfManaged) { 303 // Mark self-managed calls are read since they're being handled by their own app. 304 // Their inclusion in the call log is informational only. 305 isRead = true; 306 } 307 AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, postDialDigits, 308 viaNumber, presentation, callType, features, accountHandle, start, duration, 309 dataUsage, initiatingUser, isRead, logCallCompletedListener); 310 logCallAsync(args); 311 } else { 312 Log.d(TAG, "Not adding emergency call to call log."); 313 } 314 } 315 316 /** 317 * Based on the video state of the call, determines the call features applicable for the call. 318 * 319 * @param videoState The video state. 320 * @param isPulledCall {@code true} if this call was pulled to another device. 321 * @param isStoreHd {@code true} if this call was used HD. 322 * @param isUsingAssistedDialing {@code true} if this call used assisted dialing. 323 * @return The call features. 324 */ 325 private static int getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd, 326 boolean isUsingAssistedDialing, boolean isRtt) { 327 int features = 0; 328 if (VideoProfile.isVideo(videoState)) { 329 features |= Calls.FEATURES_VIDEO; 330 } 331 if (isPulledCall) { 332 features |= Calls.FEATURES_PULLED_EXTERNALLY; 333 } 334 if (isStoreHd) { 335 features |= Calls.FEATURES_HD_CALL; 336 } 337 if (isUsingAssistedDialing) { 338 features |= Calls.FEATURES_ASSISTED_DIALING_USED; 339 } 340 if (isRtt) { 341 features |= Calls.FEATURES_RTT; 342 } 343 return features; 344 } 345 346 private boolean shouldSaveHdInfo(Call call, PhoneAccountHandle accountHandle) { 347 CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService( 348 Context.CARRIER_CONFIG_SERVICE); 349 PersistableBundle configBundle = null; 350 if (configManager != null) { 351 configBundle = configManager.getConfigForSubId( 352 mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)); 353 } 354 if (configBundle != null && configBundle.getBoolean( 355 CarrierConfigManager.KEY_IDENTIFY_HIGH_DEFINITION_CALLS_IN_CALL_LOG_BOOL) 356 && call.wasHighDefAudio()) { 357 return true; 358 } 359 return false; 360 } 361 362 /** 363 * Retrieve the phone number from the call, and then process it before returning the 364 * actual number that is to be logged. 365 * 366 * @param call The phone connection. 367 * @return the phone number to be logged. 368 */ 369 private String getLogNumber(Call call) { 370 Uri handle = call.getOriginalHandle(); 371 372 if (handle == null) { 373 return null; 374 } 375 376 String handleString = handle.getSchemeSpecificPart(); 377 if (!PhoneNumberUtils.isUriNumber(handleString)) { 378 handleString = PhoneNumberUtils.stripSeparators(handleString); 379 } 380 return handleString; 381 } 382 383 /** 384 * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider 385 * using an AsyncTask to avoid blocking the main thread. 386 * 387 * @param args Prepopulated call details. 388 * @return A handle to the AsyncTask that will add the call to the call log asynchronously. 389 */ 390 public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) { 391 return new LogCallAsyncTask().execute(args); 392 } 393 394 /** 395 * Helper AsyncTask to access the call logs database asynchronously since database operations 396 * can take a long time depending on the system's load. Since it extends AsyncTask, it uses 397 * its own thread pool. 398 */ 399 private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> { 400 401 private LogCallCompletedListener[] mListeners; 402 403 @Override 404 protected Uri[] doInBackground(AddCallArgs... callList) { 405 int count = callList.length; 406 Uri[] result = new Uri[count]; 407 mListeners = new LogCallCompletedListener[count]; 408 for (int i = 0; i < count; i++) { 409 AddCallArgs c = callList[i]; 410 mListeners[i] = c.logCallCompletedListener; 411 try { 412 // May block. 413 result[i] = addCall(c); 414 } catch (Exception e) { 415 // This is very rare but may happen in legitimate cases. 416 // E.g. If the phone is encrypted and thus write request fails, it may cause 417 // some kind of Exception (right now it is IllegalArgumentException, but this 418 // might change). 419 // 420 // We don't want to crash the whole process just because of that, so just log 421 // it instead. 422 Log.e(TAG, e, "Exception raised during adding CallLog entry."); 423 result[i] = null; 424 } 425 } 426 return result; 427 } 428 429 private Uri addCall(AddCallArgs c) { 430 PhoneAccount phoneAccount = mPhoneAccountRegistrar 431 .getPhoneAccountUnchecked(c.accountHandle); 432 if (phoneAccount != null && 433 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) { 434 if (c.initiatingUser != null && 435 UserUtil.isManagedProfile(mContext, c.initiatingUser)) { 436 return addCall(c, c.initiatingUser); 437 } else { 438 return addCall(c, null); 439 } 440 } else { 441 return addCall(c, c.accountHandle == null ? null : c.accountHandle.getUserHandle()); 442 } 443 } 444 445 /** 446 * Insert the call to a specific user or all users except managed profile. 447 * @param c context 448 * @param userToBeInserted user handle of user that the call going be inserted to. null 449 * if insert to all users except managed profile. 450 */ 451 private Uri addCall(AddCallArgs c, UserHandle userToBeInserted) { 452 return Calls.addCall(c.callerInfo, c.context, c.number, c.postDialDigits, c.viaNumber, 453 c.presentation, c.callType, c.features, c.accountHandle, c.timestamp, 454 c.durationInSec, c.dataUsage, userToBeInserted == null, 455 userToBeInserted, c.isRead); 456 } 457 458 459 @Override 460 protected void onPostExecute(Uri[] result) { 461 for (int i = 0; i < result.length; i++) { 462 Uri uri = result[i]; 463 /* 464 Performs a simple sanity check to make sure the call was written in the database. 465 Typically there is only one result per call so it is easy to identify which one 466 failed. 467 */ 468 if (uri == null) { 469 Log.w(TAG, "Failed to write call to the log."); 470 } 471 if (mListeners[i] != null) { 472 mListeners[i].onLogCompleted(uri); 473 } 474 } 475 } 476 } 477 478 private void sendAddCallBroadcast(int callType, long duration) { 479 Intent callAddIntent = new Intent(ACTION_CALLS_TABLE_ADD_ENTRY); 480 callAddIntent.putExtra(CALL_TYPE, callType); 481 callAddIntent.putExtra(CALL_DURATION, duration); 482 mContext.sendBroadcast(callAddIntent, PERMISSION_PROCESS_CALLLOG_INFO); 483 } 484 485 private String getCountryIsoFromCountry(Country country) { 486 if(country == null) { 487 // Fallback to Locale if there are issues with CountryDetector 488 Log.w(TAG, "Value for country was null. Falling back to Locale."); 489 return Locale.getDefault().getCountry(); 490 } 491 492 return country.getCountryIso(); 493 } 494 495 /** 496 * Get the current country code 497 * 498 * @return the ISO 3166-1 two letters country code of current country. 499 */ 500 public String getCountryIso() { 501 synchronized (mLock) { 502 if (mCurrentCountryIso == null) { 503 Log.i(TAG, "Country cache is null. Detecting Country and Setting Cache..."); 504 final CountryDetector countryDetector = 505 (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR); 506 Country country = null; 507 if (countryDetector != null) { 508 country = countryDetector.detectCountry(); 509 510 countryDetector.addCountryListener((newCountry) -> { 511 Log.startSession("CLM.oCD"); 512 try { 513 synchronized (mLock) { 514 Log.i(TAG, "Country ISO changed. Retrieving new ISO..."); 515 mCurrentCountryIso = getCountryIsoFromCountry(newCountry); 516 } 517 } finally { 518 Log.endSession(); 519 } 520 }, Looper.getMainLooper()); 521 } 522 mCurrentCountryIso = getCountryIsoFromCountry(country); 523 } 524 return mCurrentCountryIso; 525 } 526 } 527 } 528