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