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