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