CallLogManager.java revision 3037ac6f171b6a3627494bb10042ab7adb34366a
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( 176 new MissedCallNotifier.CallInfo(call)); 177 } 178 }); 179 } else { 180 logCall(call, type, null); 181 } 182 } 183 184 /** 185 * Logs a call to the call log based on the {@link Call} object passed in. 186 * 187 * @param call The call object being logged 188 * @param callLogType The type of call log entry to log this call as. See: 189 * {@link android.provider.CallLog.Calls#INCOMING_TYPE} 190 * {@link android.provider.CallLog.Calls#OUTGOING_TYPE} 191 * {@link android.provider.CallLog.Calls#MISSED_TYPE} 192 * @param logCallCompletedListener optional callback called after the call is logged. 193 */ 194 void logCall(Call call, int callLogType, 195 @Nullable LogCallCompletedListener logCallCompletedListener) { 196 final long creationTime = call.getCreationTimeMillis(); 197 final long age = call.getAgeMillis(); 198 199 final String logNumber = getLogNumber(call); 200 201 Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber)); 202 203 final PhoneAccountHandle emergencyAccountHandle = 204 TelephonyUtil.getDefaultEmergencyPhoneAccount().getAccountHandle(); 205 206 String formattedViaNumber = PhoneNumberUtils.formatNumber(call.getViaNumber(), 207 getCountryIso()); 208 formattedViaNumber = (formattedViaNumber != null) ? 209 formattedViaNumber : call.getViaNumber(); 210 211 PhoneAccountHandle accountHandle = call.getTargetPhoneAccount(); 212 if (emergencyAccountHandle.equals(accountHandle)) { 213 accountHandle = null; 214 } 215 216 Long callDataUsage = call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET ? null : 217 call.getCallDataUsage(); 218 219 int callFeatures = getCallFeatures(call.getVideoStateHistory(), 220 call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED); 221 logCall(call.getCallerInfo(), logNumber, call.getPostDialDigits(), formattedViaNumber, 222 call.getHandlePresentation(), callLogType, callFeatures, accountHandle, 223 creationTime, age, callDataUsage, call.isEmergencyCall(), call.getInitiatingUser(), 224 logCallCompletedListener); 225 } 226 227 /** 228 * Inserts a call into the call log, based on the parameters passed in. 229 * 230 * @param callerInfo Caller details. 231 * @param number The number the call was made to or from. 232 * @param postDialDigits The post-dial digits that were dialed after the number, 233 * if it was an outgoing call. Otherwise ''. 234 * @param presentation 235 * @param callType The type of call. 236 * @param features The features of the call. 237 * @param start The start time of the call, in milliseconds. 238 * @param duration The duration of the call, in milliseconds. 239 * @param dataUsage The data usage for the call, null if not applicable. 240 * @param isEmergency {@code true} if this is an emergency call, {@code false} otherwise. 241 * @param logCallCompletedListener optional callback called after the call is logged. 242 */ 243 private void logCall( 244 CallerInfo callerInfo, 245 String number, 246 String postDialDigits, 247 String viaNumber, 248 int presentation, 249 int callType, 250 int features, 251 PhoneAccountHandle accountHandle, 252 long start, 253 long duration, 254 Long dataUsage, 255 boolean isEmergency, 256 UserHandle initiatingUser, 257 @Nullable LogCallCompletedListener logCallCompletedListener) { 258 259 // On some devices, to avoid accidental redialing of emergency numbers, we *never* log 260 // emergency calls to the Call Log. (This behavior is set on a per-product basis, based 261 // on carrier requirements.) 262 boolean okToLogEmergencyNumber = false; 263 CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService( 264 Context.CARRIER_CONFIG_SERVICE); 265 PersistableBundle configBundle = configManager.getConfig(); 266 if (configBundle != null) { 267 okToLogEmergencyNumber = configBundle.getBoolean( 268 CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL); 269 } 270 271 // Don't log emergency numbers if the device doesn't allow it. 272 final boolean isOkToLogThisCall = !isEmergency || okToLogEmergencyNumber; 273 274 sendAddCallBroadcast(callType, duration); 275 276 if (isOkToLogThisCall) { 277 Log.d(TAG, "Logging Calllog entry: " + callerInfo + ", " 278 + Log.pii(number) + "," + presentation + ", " + callType 279 + ", " + start + ", " + duration); 280 AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, postDialDigits, 281 viaNumber, presentation, callType, features, accountHandle, start, duration, 282 dataUsage, initiatingUser, logCallCompletedListener); 283 logCallAsync(args); 284 } else { 285 Log.d(TAG, "Not adding emergency call to call log."); 286 } 287 } 288 289 /** 290 * Based on the video state of the call, determines the call features applicable for the call. 291 * 292 * @param videoState The video state. 293 * @param isPulledCall {@code true} if this call was pulled to another device. 294 * @return The call features. 295 */ 296 private static int getCallFeatures(int videoState, boolean isPulledCall) { 297 int features = 0; 298 if (VideoProfile.isVideo(videoState)) { 299 features |= Calls.FEATURES_VIDEO; 300 } 301 if (isPulledCall) { 302 features |= Calls.FEATURES_PULLED_EXTERNALLY; 303 } 304 return features; 305 } 306 307 /** 308 * Retrieve the phone number from the call, and then process it before returning the 309 * actual number that is to be logged. 310 * 311 * @param call The phone connection. 312 * @return the phone number to be logged. 313 */ 314 private String getLogNumber(Call call) { 315 Uri handle = call.getOriginalHandle(); 316 317 if (handle == null) { 318 return null; 319 } 320 321 String handleString = handle.getSchemeSpecificPart(); 322 if (!PhoneNumberUtils.isUriNumber(handleString)) { 323 handleString = PhoneNumberUtils.stripSeparators(handleString); 324 } 325 return handleString; 326 } 327 328 /** 329 * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider 330 * using an AsyncTask to avoid blocking the main thread. 331 * 332 * @param args Prepopulated call details. 333 * @return A handle to the AsyncTask that will add the call to the call log asynchronously. 334 */ 335 public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) { 336 return new LogCallAsyncTask().execute(args); 337 } 338 339 /** 340 * Helper AsyncTask to access the call logs database asynchronously since database operations 341 * can take a long time depending on the system's load. Since it extends AsyncTask, it uses 342 * its own thread pool. 343 */ 344 private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> { 345 346 private LogCallCompletedListener[] mListeners; 347 348 @Override 349 protected Uri[] doInBackground(AddCallArgs... callList) { 350 int count = callList.length; 351 Uri[] result = new Uri[count]; 352 mListeners = new LogCallCompletedListener[count]; 353 for (int i = 0; i < count; i++) { 354 AddCallArgs c = callList[i]; 355 mListeners[i] = c.logCallCompletedListener; 356 try { 357 // May block. 358 result[i] = addCall(c); 359 } catch (Exception e) { 360 // This is very rare but may happen in legitimate cases. 361 // E.g. If the phone is encrypted and thus write request fails, it may cause 362 // some kind of Exception (right now it is IllegalArgumentException, but this 363 // might change). 364 // 365 // We don't want to crash the whole process just because of that, so just log 366 // it instead. 367 Log.e(TAG, e, "Exception raised during adding CallLog entry."); 368 result[i] = null; 369 } 370 } 371 return result; 372 } 373 374 private Uri addCall(AddCallArgs c) { 375 PhoneAccount phoneAccount = mPhoneAccountRegistrar 376 .getPhoneAccountUnchecked(c.accountHandle); 377 if (phoneAccount != null && 378 phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) { 379 if (c.initiatingUser != null && 380 UserUtil.isManagedProfile(mContext, c.initiatingUser)) { 381 return addCall(c, c.initiatingUser); 382 } else { 383 return addCall(c, null); 384 } 385 } else { 386 return addCall(c, c.accountHandle == null ? null : c.accountHandle.getUserHandle()); 387 } 388 } 389 390 /** 391 * Insert the call to a specific user or all users except managed profile. 392 * @param c context 393 * @param userToBeInserted user handle of user that the call going be inserted to. null 394 * if insert to all users except managed profile. 395 */ 396 private Uri addCall(AddCallArgs c, UserHandle userToBeInserted) { 397 return Calls.addCall(c.callerInfo, c.context, c.number, c.postDialDigits, c.viaNumber, 398 c.presentation, c.callType, c.features, c.accountHandle, c.timestamp, 399 c.durationInSec, c.dataUsage, userToBeInserted == null, 400 userToBeInserted); 401 } 402 403 404 @Override 405 protected void onPostExecute(Uri[] result) { 406 for (int i = 0; i < result.length; i++) { 407 Uri uri = result[i]; 408 /* 409 Performs a simple sanity check to make sure the call was written in the database. 410 Typically there is only one result per call so it is easy to identify which one 411 failed. 412 */ 413 if (uri == null) { 414 Log.w(TAG, "Failed to write call to the log."); 415 } 416 if (mListeners[i] != null) { 417 mListeners[i].onLogCompleted(uri); 418 } 419 } 420 } 421 } 422 423 private void sendAddCallBroadcast(int callType, long duration) { 424 Intent callAddIntent = new Intent(ACTION_CALLS_TABLE_ADD_ENTRY); 425 callAddIntent.putExtra(CALL_TYPE, callType); 426 callAddIntent.putExtra(CALL_DURATION, duration); 427 mContext.sendBroadcast(callAddIntent, PERMISSION_PROCESS_CALLLOG_INFO); 428 } 429 430 private String getCountryIsoFromCountry(Country country) { 431 if(country == null) { 432 // Fallback to Locale if there are issues with CountryDetector 433 Log.w(TAG, "Value for country was null. Falling back to Locale."); 434 return Locale.getDefault().getCountry(); 435 } 436 437 return country.getCountryIso(); 438 } 439 440 /** 441 * Get the current country code 442 * 443 * @return the ISO 3166-1 two letters country code of current country. 444 */ 445 public String getCountryIso() { 446 synchronized (mLock) { 447 if (mCurrentCountryIso == null) { 448 Log.i(TAG, "Country cache is null. Detecting Country and Setting Cache..."); 449 final CountryDetector countryDetector = 450 (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR); 451 Country country = null; 452 if (countryDetector != null) { 453 country = countryDetector.detectCountry(); 454 455 countryDetector.addCountryListener((newCountry) -> { 456 Log.startSession("CLM.oCD"); 457 try { 458 synchronized (mLock) { 459 Log.i(TAG, "Country ISO changed. Retrieving new ISO..."); 460 mCurrentCountryIso = getCountryIsoFromCountry(newCountry); 461 } 462 } finally { 463 Log.endSession(); 464 } 465 }, Looper.getMainLooper()); 466 } 467 mCurrentCountryIso = getCountryIsoFromCountry(country); 468 } 469 return mCurrentCountryIso; 470 } 471 } 472} 473