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