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