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