| /* |
| * Copyright 2014, The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.telecom; |
| |
| import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED; |
| import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL; |
| import static com.android.server.telecom.util.CallLogUtils.AddCallParams.AddCallParametersBuilder.MAX_NUMBER_OF_CHARACTERS; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.database.Cursor; |
| import android.location.Location; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.UserHandle; |
| import android.os.PersistableBundle; |
| import android.os.UserManager; |
| import android.provider.CallLog; |
| import android.provider.CallLog.Calls; |
| import android.telecom.Connection; |
| import android.telecom.DisconnectCause; |
| import android.telecom.Log; |
| import android.telecom.PhoneAccount; |
| import android.telecom.PhoneAccountHandle; |
| import android.telecom.TelecomManager; |
| import android.telecom.VideoProfile; |
| import android.telephony.CarrierConfigManager; |
| import android.telephony.PhoneNumberUtils; |
| import android.telephony.SubscriptionManager; |
| import android.telephony.TelephonyManager; |
| import android.util.Pair; |
| import android.text.TextUtils; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.modules.utils.HandlerExecutor; |
| import com.android.server.telecom.callfiltering.CallFilteringResult; |
| import com.android.server.telecom.flags.FeatureFlags; |
| import com.android.server.telecom.flags.Flags; |
| import com.android.server.telecom.util.CallLogUtils; |
| import com.android.server.telecom.util.CallerInfo; |
| |
| import java.util.Arrays; |
| import java.util.Locale; |
| import java.util.Objects; |
| import java.util.UUID; |
| import java.util.stream.Stream; |
| |
| /** |
| * Helper class that provides functionality to write information about calls and their associated |
| * caller details to the call log. All logging activity will be performed asynchronously in a |
| * background thread to avoid blocking on the main thread. |
| */ |
| @VisibleForTesting |
| public final class CallLogManager extends CallsManagerListenerBase { |
| |
| public interface LogCallCompletedListener { |
| void onLogCompleted(@Nullable Uri uri); |
| } |
| |
| /** |
| * Parameter object to hold the arguments to add a call in the call log DB. |
| */ |
| private static class AddCallArgs { |
| public AddCallArgs(Context context, CallLogUtils.AddCallParams params, |
| @Nullable LogCallCompletedListener logCallCompletedListener, |
| @NonNull Call call) { |
| this.context = context; |
| this.params = params; |
| this.logCallCompletedListener = logCallCompletedListener; |
| this.call = call; |
| |
| } |
| // Since the members are accessed directly, we don't use the |
| // mXxxx notation. |
| public final Context context; |
| public final CallLogUtils.AddCallParams params; |
| public final Call call; |
| @Nullable |
| public final LogCallCompletedListener logCallCompletedListener; |
| } |
| |
| private static final String TAG = CallLogManager.class.getSimpleName(); |
| |
| // Copied from android.telephony.DisconnectCause.toString |
| // TODO: come up with a better way to indicate in a android.telecom.DisconnectCause that |
| // a conference was merged successfully |
| private static final String REASON_IMS_MERGED_SUCCESSFULLY = "IMS_MERGED_SUCCESSFULLY"; |
| private static final UUID LOG_CALL_FAILED_ANOMALY_ID = |
| UUID.fromString("d9b38771-ff36-417b-8723-2363a870c702"); |
| private static final String LOG_CALL_FAILED_ANOMALY_DESC = |
| "Based on the current user, Telecom detected failure to record a call to the call log."; |
| |
| private final Context mContext; |
| private final CarrierConfigManager mCarrierConfigManager; |
| private final PhoneAccountRegistrar mPhoneAccountRegistrar; |
| private final MissedCallNotifier mMissedCallNotifier; |
| private AnomalyReporterAdapter mAnomalyReporterAdapter; |
| private static final String ACTION_CALLS_TABLE_ADD_ENTRY = |
| "com.android.server.telecom.intent.action.CALLS_ADD_ENTRY"; |
| private static final String PERMISSION_PROCESS_CALLLOG_INFO = |
| "android.permission.PROCESS_CALLLOG_INFO"; |
| private static final String CALL_TYPE = "callType"; |
| private static final String CALL_DURATION = "duration"; |
| |
| private final Object mLock = new Object(); |
| private String mCurrentCountryIso; |
| private HandlerExecutor mCountryCodeExecutor; |
| |
| private final FeatureFlags mFeatureFlags; |
| |
| public CallLogManager(Context context, PhoneAccountRegistrar phoneAccountRegistrar, |
| MissedCallNotifier missedCallNotifier, AnomalyReporterAdapter anomalyReporterAdapter, |
| FeatureFlags featureFlags) { |
| mContext = context; |
| mCarrierConfigManager = (CarrierConfigManager) mContext |
| .getSystemService(Context.CARRIER_CONFIG_SERVICE); |
| mPhoneAccountRegistrar = phoneAccountRegistrar; |
| mMissedCallNotifier = missedCallNotifier; |
| mAnomalyReporterAdapter = anomalyReporterAdapter; |
| mCountryCodeExecutor = new HandlerExecutor(new Handler(Looper.getMainLooper())); |
| mFeatureFlags = featureFlags; |
| } |
| |
| @Override |
| public void onCallStateChanged(Call call, int oldState, int newState) { |
| int disconnectCause = call.getDisconnectCause().getCode(); |
| boolean isNewlyDisconnected = |
| newState == CallState.DISCONNECTED || newState == CallState.ABORTED; |
| boolean isCallCanceled = isNewlyDisconnected && disconnectCause == DisconnectCause.CANCELED; |
| |
| if (!isNewlyDisconnected) { |
| return; |
| } |
| |
| if (shouldLogDisconnectedCall(call, oldState, isCallCanceled)) { |
| Log.i(this, "onCallStateChanged: call=%s, newState=%s, disconnectCause=%s", |
| call.getId(), |
| CallState.toString(newState), |
| DisconnectCause.disconnectCodeToString(disconnectCause)); |
| int type; |
| if (!call.isIncoming()) { |
| type = Calls.OUTGOING_TYPE; |
| } else if (disconnectCause == DisconnectCause.MISSED) { |
| type = Calls.MISSED_TYPE; |
| } else if (disconnectCause == DisconnectCause.ANSWERED_ELSEWHERE) { |
| type = Calls.ANSWERED_EXTERNALLY_TYPE; |
| } else if (disconnectCause == DisconnectCause.REJECTED) { |
| type = Calls.REJECTED_TYPE; |
| } else { |
| type = Calls.INCOMING_TYPE; |
| } |
| |
| // Always show the notification for managed calls. For self-managed calls, it is up to |
| // the app to show the notification, so suppress the notification when logging the call. |
| boolean showNotification = call.isManaged(); |
| logCall(call, type, showNotification, null /*result*/); |
| } |
| } |
| |
| /** |
| * Log call only if Call is NOT a self-managed call OR call is a self-managed call which has |
| * indicated it should be logged in its PhoneAccount |
| */ |
| void logCallIfNotSelfManaged (Call call, int type, boolean showNotificationForMissedCall, |
| CallFilteringResult result) { |
| boolean shouldCallSelfManagedLogged = shouldLogVoipCall(call); |
| if (call.isManaged() || shouldCallSelfManagedLogged) { |
| logCall(call, type, showNotificationForMissedCall, result); |
| } |
| } |
| |
| /** |
| * Log newly disconnected calls only if all of below conditions are met: |
| * Call was NOT in the "choose account" phase when disconnected |
| * Call is NOT a conference call which had children (unless it was remotely hosted). |
| * Call is NOT a child call from a conference which was remotely hosted. |
| * Call has NOT indicated it should be skipped for logging in its extras |
| * Call is NOT simulating a single party conference. |
| * Call was NOT explicitly canceled, except for disconnecting from a conference. |
| * Call is NOT an external call or an external call on watch. |
| * Call is NOT disconnected because of merging into a conference. |
| * Call is NOT a self-managed call OR call is a self-managed call which has indicated it |
| * should be logged in its PhoneAccount |
| */ |
| @VisibleForTesting |
| public boolean shouldLogDisconnectedCall(Call call, int oldState, boolean isCallCanceled) { |
| boolean shouldCallSelfManagedLogged = shouldLogVoipCall(call); |
| |
| // "Choose account" phase when disconnected |
| if (oldState == CallState.SELECT_PHONE_ACCOUNT) { |
| return false; |
| } |
| // A conference call which had children should not be logged, unless it was remotely hosted. |
| if (call.isConference() && call.hadChildren() && |
| !call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) { |
| return false; |
| } |
| |
| // A conference call which had no children should not be logged; this case will occur on IMS |
| // when no conference event package data is received. We will have logged the participants |
| // as they merge into the conference, so we should not log the conference itself. |
| if (call.isConference() && !call.hadChildren() && |
| !call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) { |
| return false; |
| } |
| |
| if (call.getExtras() != null |
| && call.getExtras().containsKey(TelecomManager.EXTRA_DO_NOT_LOG_CALL)) { |
| return false; |
| } |
| |
| // A child call of a conference which was remotely hosted; these didn't originate on this |
| // device and should not be logged. |
| if (call.getParentCall() != null && call.hasProperty(Connection.PROPERTY_REMOTELY_HOSTED)) { |
| return false; |
| } |
| |
| DisconnectCause cause = call.getDisconnectCause(); |
| if (isCallCanceled) { |
| // No log when disconnecting to simulate a single party conference. |
| if (cause != null |
| && DisconnectCause.REASON_EMULATING_SINGLE_CALL.equals(cause.getReason())) { |
| return false; |
| } |
| // Explicitly canceled |
| // Conference children connections only have CAPABILITY_DISCONNECT_FROM_CONFERENCE. |
| // Log them when they are disconnected from conference. |
| return (call.getConnectionCapabilities() |
| & Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE) |
| == Connection.CAPABILITY_DISCONNECT_FROM_CONFERENCE; |
| } |
| // An external and non-watch call |
| if (call.isExternalCall() && !mContext.getPackageManager().hasSystemFeature( |
| PackageManager.FEATURE_WATCH)) { |
| return false; |
| } |
| |
| // Call merged into conferences and marked with IMS_MERGED_SUCCESSFULLY. |
| // Return false if the conference supports the participants packets for the carrier. |
| // Otherwise, fall through. Merged calls would be associated with disconnected |
| // connections because of special carrier requirements. Those calls don't look like |
| // merged, e.g. could be one active and the other on hold. |
| if (cause != null && REASON_IMS_MERGED_SUCCESSFULLY.equals(cause.getReason())) { |
| int subscriptionId = mPhoneAccountRegistrar |
| .getSubscriptionIdForPhoneAccount(call.getTargetPhoneAccount()); |
| // By default, the conference should return a list of participants. |
| if (subscriptionId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { |
| return false; |
| } |
| |
| if (mCarrierConfigManager == null) { |
| return false; |
| } |
| PersistableBundle b = mCarrierConfigManager.getConfigForSubId(subscriptionId); |
| if (b == null || b.getBoolean(KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL, true)) { |
| return false; |
| } |
| } |
| |
| // Call is NOT a self-managed call OR call is a self-managed call which has indicated it |
| // should be logged in its PhoneAccount |
| return call.isManaged() || shouldCallSelfManagedLogged; |
| } |
| |
| void logCall(Call call, int type, boolean showNotificationForMissedCall, CallFilteringResult |
| result) { |
| if ((type == Calls.MISSED_TYPE || type == Calls.BLOCKED_TYPE) && |
| showNotificationForMissedCall) { |
| logCall(call, type, new LogCallCompletedListener() { |
| @Override |
| public void onLogCompleted(@Nullable Uri uri) { |
| if (mFeatureFlags.addCallUriForMissedCalls()){ |
| mMissedCallNotifier.showMissedCallNotification( |
| new MissedCallNotifier.CallInfo(call), uri); |
| } else { |
| mMissedCallNotifier.showMissedCallNotification( |
| new MissedCallNotifier.CallInfo(call), /* uri= */ null); |
| } |
| } |
| }, result); |
| } else { |
| logCall(call, type, null, result); |
| } |
| } |
| |
| /** |
| * Logs a call to the call log based on the {@link Call} object passed in. |
| * |
| * @param call The call object being logged |
| * @param callLogType The type of call log entry to log this call as. See: |
| * {@link android.provider.CallLog.Calls#INCOMING_TYPE} |
| * {@link android.provider.CallLog.Calls#OUTGOING_TYPE} |
| * {@link android.provider.CallLog.Calls#MISSED_TYPE} |
| * {@link android.provider.CallLog.Calls#BLOCKED_TYPE} |
| * @param logCallCompletedListener optional callback called after the call is logged. |
| * @param result is generated when call type is |
| * {@link android.provider.CallLog.Calls#BLOCKED_TYPE}. |
| */ |
| @VisibleForTesting |
| public void logCall(Call call, int callLogType, |
| @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) { |
| // If the call has already been logged, do not log it again. This is an atomic check-and-set |
| // to prevent race conditions from multiple disconnect events. |
| if (call.getAndSetHasBeenLogged()) { |
| Log.i(TAG, "LogCall: skipping already-logged call: %s", call.getId()); |
| return; |
| } |
| CallLogUtils.AddCallParams.AddCallParametersBuilder paramBuilder = |
| new CallLogUtils.AddCallParams.AddCallParametersBuilder(); |
| if (call.getConnectTimeMillis() != 0 |
| && call.getConnectTimeMillis() < call.getCreationTimeMillis()) { |
| // If connected time is available, use connected time. The connected time might be |
| // earlier than created time since it might come from carrier sent special SMS to |
| // notifier user earlier missed call. |
| paramBuilder.setStart(call.getConnectTimeMillis()); |
| } else { |
| paramBuilder.setStart(call.getCreationTimeMillis()); |
| } |
| |
| paramBuilder.setDuration((int) (call.getAgeMillis() / 1000)); |
| |
| String logNumber = getLogNumber(call); |
| paramBuilder.setNumber(logNumber); |
| |
| Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber)); |
| |
| String formattedViaNumber = PhoneNumberUtils.formatNumber(call.getViaNumber(), |
| getCountryIso()); |
| formattedViaNumber = (formattedViaNumber != null) ? |
| formattedViaNumber : call.getViaNumber(); |
| paramBuilder.setViaNumber(formattedViaNumber); |
| |
| final PhoneAccountHandle emergencyAccountHandle = |
| TelephonyUtil.getDefaultEmergencyPhoneAccount().getAccountHandle(); |
| PhoneAccountHandle accountHandle = call.getTargetPhoneAccount(); |
| if (emergencyAccountHandle.equals(accountHandle)) { |
| accountHandle = null; |
| } |
| paramBuilder.setAccountHandle(accountHandle); |
| |
| paramBuilder.setDataUsage(call.getCallDataUsage() == Call.DATA_USAGE_NOT_SET |
| ? Long.MIN_VALUE : call.getCallDataUsage()); |
| |
| paramBuilder.setFeatures(getCallFeatures(call.getVideoStateHistory(), |
| call.getDisconnectCause().getCode() == DisconnectCause.CALL_PULLED, |
| call.wasHighDefAudio(), call.wasWifi(), |
| (call.getConnectionProperties() & Connection.PROPERTY_ASSISTED_DIALING) == |
| Connection.PROPERTY_ASSISTED_DIALING, |
| call.wasEverRttCall(), |
| call.wasVolte(), |
| call.wasVonr(), |
| call.isHdPlus(), |
| call.isGroupCall(), mFeatureFlags)); |
| |
| if (result == null) { |
| result = new CallFilteringResult.Builder() |
| .setCallScreeningAppName(call.getCallScreeningAppName()) |
| .setCallScreeningComponentName(call.getCallScreeningComponentName()) |
| .build(); |
| } |
| if (callLogType == Calls.BLOCKED_TYPE || callLogType == Calls.MISSED_TYPE) { |
| paramBuilder.setCallBlockReason(result.mCallBlockReason); |
| paramBuilder.setCallScreeningComponentName(result.mCallScreeningComponentName); |
| paramBuilder.setCallScreeningAppName(result.mCallScreeningAppName); |
| } else { |
| paramBuilder.setCallBlockReason(BLOCK_REASON_NOT_BLOCKED); |
| } |
| |
| PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(accountHandle); |
| UserHandle initiatingUser = call.getAssociatedUser(); |
| if (phoneAccount != null && |
| phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) { |
| if (initiatingUser != null && |
| UserUtil.isProfile(mContext, initiatingUser)) { |
| paramBuilder.setUserToBeInsertedTo(initiatingUser); |
| paramBuilder.setAddForAllUsers(false); |
| } else { |
| paramBuilder.setAddForAllUsers(true); |
| } |
| } else { |
| if (accountHandle == null) { |
| paramBuilder.setAddForAllUsers(true); |
| } else { |
| paramBuilder.setUserToBeInsertedTo(accountHandle.getUserHandle()); |
| paramBuilder.setAddForAllUsers(accountHandle.getUserHandle() == null); |
| } |
| } |
| if (call.getIntentExtras() != null) { |
| if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_PRIORITY)) { |
| paramBuilder.setPriority(call.getIntentExtras() |
| .getInt(TelecomManager.EXTRA_PRIORITY)); |
| } |
| if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_CALL_SUBJECT)) { |
| paramBuilder.setSubject(call.getIntentExtras() |
| .getString(TelecomManager.EXTRA_CALL_SUBJECT)); |
| } |
| if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_PICTURE_URI)) { |
| paramBuilder.setPictureUri(call.getIntentExtras() |
| .getParcelable(TelecomManager.EXTRA_PICTURE_URI)); |
| } |
| // The picture uri can end up either in extras or in intent extras due to how these |
| // two bundles are set. For incoming calls they're in extras, but for outgoing calls |
| // they're in intentExtras. |
| if (call.getExtras() != null |
| && call.getExtras().containsKey(TelecomManager.EXTRA_PICTURE_URI)) { |
| paramBuilder.setPictureUri(call.getExtras() |
| .getParcelable(TelecomManager.EXTRA_PICTURE_URI)); |
| } |
| if (call.getIntentExtras().containsKey(TelecomManager.EXTRA_LOCATION)) { |
| Location l = call.getIntentExtras().getParcelable(TelecomManager.EXTRA_LOCATION); |
| if (l != null) { |
| paramBuilder.setLatitude(l.getLatitude()); |
| paramBuilder.setLongitude(l.getLongitude()); |
| } |
| } |
| } |
| |
| paramBuilder.setPostDialDigits(call.getPostDialDigits()); |
| paramBuilder.setPresentation(call.getHandlePresentation()); |
| paramBuilder.setCallType(callLogType); |
| paramBuilder.setIsRead(!call.isManaged()); |
| paramBuilder.setMissedReason(call.getMissedReason()); |
| if (mFeatureFlags.businessCallComposer() && call.getExtras() != null) { |
| Bundle extras = call.getExtras(); |
| boolean isBusinessCall = |
| extras.getBoolean(android.telecom.Call.EXTRA_IS_BUSINESS_CALL, false); |
| paramBuilder.setIsBusinessCall(isBusinessCall); |
| if (isBusinessCall) { |
| Log.i(TAG, "logging business call"); |
| String assertedDisplayName = |
| extras.getString(android.telecom.Call.EXTRA_ASSERTED_DISPLAY_NAME, ""); |
| if (assertedDisplayName.length() > MAX_NUMBER_OF_CHARACTERS) { |
| // avoid throwing an IllegalArgumentException and only log the first 256 |
| // characters of the name. |
| paramBuilder.setAssertedDisplayName( |
| assertedDisplayName.substring(0, MAX_NUMBER_OF_CHARACTERS)); |
| } else { |
| paramBuilder.setAssertedDisplayName(assertedDisplayName); |
| } |
| } |
| } |
| |
| CallerInfo callerInfo = call.getCallerInfo(); |
| boolean isCallerDisplayPresent = call.getCallerDisplayName() != null |
| && !call.getCallerDisplayName().isEmpty(); |
| // At this point, we have already checked to see if we should log a transactional call. |
| if (mFeatureFlags.integratedCallLogs() && call.isTransactionalCall()) { |
| paramBuilder.setUuid(call.getId()); |
| if (isCallerDisplayPresent && callerInfo != null) { |
| callerInfo.setName(call.getCallerDisplayName()); |
| } |
| // Sets the VoIP contact lookup uri. |
| if (android.telecom.flags.Flags.integratedCallLogsStage2()) { |
| paramBuilder.setVoipContactLookupUri(call.getVoipContactLookupUri()); |
| } |
| } |
| // A little different from the above logic to set the caller info name to the caller display |
| // name as that field is used for populating the CACHED_NAME column in the call log, which |
| // may be overwritten if a contact exists. |
| if (mFeatureFlags.supportDisplayNameCallLog()) { |
| String preferredName = getPreferredName(call, isCallerDisplayPresent, callerInfo); |
| paramBuilder.setPreferredDisplayName(preferredName); |
| String name = callerInfo != null ? callerInfo.getName() : ""; |
| Log.w(TAG, "Call display name details - [display name: %s, preferred display name: %s]", |
| Log.pii(name), Log.pii(preferredName)); |
| } |
| paramBuilder.setCallerInfo(callerInfo); |
| |
| sendAddCallBroadcast(callLogType, call.getAgeMillis()); |
| |
| boolean okayToLog = |
| okayToLogCall(accountHandle, logNumber, call.isEmergencyCall()); |
| if (okayToLog) { |
| AddCallArgs args = new AddCallArgs(mContext, paramBuilder.build(), |
| logCallCompletedListener, call); |
| Log.addEvent(call, LogUtils.Events.LOG_CALL, "number=" + Log.piiHandle(logNumber) |
| + ",postDial=" + Log.piiHandle(call.getPostDialDigits()) + ",pres=" |
| + call.getHandlePresentation() |
| + ",code=" + DisconnectCause.disconnectCodeToString( |
| call.getDisconnectCause().getCode())); |
| logCallAsync(args); |
| } else { |
| Log.addEvent(call, LogUtils.Events.SKIP_CALL_LOG); |
| } |
| } |
| |
| private static String getPreferredName(Call call, boolean isCallerDisplayPresent, |
| CallerInfo callerInfo) { |
| if (isCallerDisplayPresent) { |
| if (call.getCallerDisplayNamePresentation() == TelecomManager.PRESENTATION_ALLOWED) { |
| return call.getCallerDisplayName(); |
| } |
| Log.w(TAG, "Clearing caller display name due to presentation restriction"); |
| } else if (callerInfo != null) { |
| if (callerInfo.namePresentation == TelecomManager.PRESENTATION_ALLOWED) { |
| return callerInfo.cnapName; |
| } |
| Log.w(TAG, "Clearing cnapName due to presentation restriction"); |
| } |
| return ""; |
| } |
| |
| boolean okayToLogCall(PhoneAccountHandle accountHandle, String number, boolean isEmergency) { |
| // On some devices, to avoid accidental redialing of emergency numbers, we *never* log |
| // emergency calls to the Call Log. (This behavior is set on a per-product basis, based |
| // on carrier requirements.) |
| boolean okToLogEmergencyNumber = false; |
| CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService( |
| Context.CARRIER_CONFIG_SERVICE); |
| PersistableBundle configBundle = (configManager != null) ? configManager.getConfigForSubId( |
| mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle)) : null; |
| if (configBundle != null) { |
| okToLogEmergencyNumber = configBundle.getBoolean( |
| CarrierConfigManager.KEY_ALLOW_EMERGENCY_NUMBERS_IN_CALL_LOG_BOOL); |
| } |
| |
| // Don't log emergency numbers if the device doesn't allow it. |
| return (!isEmergency || okToLogEmergencyNumber) |
| && !isUnloggableNumber(number, configBundle); |
| } |
| |
| private boolean isUnloggableNumber(String callNumber, PersistableBundle carrierConfig) { |
| String normalizedNumber = PhoneNumberUtils.normalizeNumber(callNumber); |
| String[] unloggableNumbersFromCarrierConfig = carrierConfig == null ? null |
| : carrierConfig.getStringArray( |
| CarrierConfigManager.KEY_UNLOGGABLE_NUMBERS_STRING_ARRAY); |
| String[] unloggableNumbersFromMccConfig; |
| unloggableNumbersFromMccConfig = TelecomResourceId.getStringArray(mContext, |
| "unloggable_phone_numbers"); |
| return Stream.concat( |
| unloggableNumbersFromCarrierConfig == null ? |
| Stream.empty() : Arrays.stream(unloggableNumbersFromCarrierConfig), |
| unloggableNumbersFromMccConfig == null ? |
| Stream.empty() : Arrays.stream(unloggableNumbersFromMccConfig) |
| ).anyMatch(unloggableNumber -> Objects.equals(unloggableNumber, normalizedNumber)); |
| } |
| |
| /** |
| * Based on the video state of the call, determines the call features applicable for the call. |
| * |
| * @param videoState The video state. |
| * @param isPulledCall {@code true} if this call was pulled to another device. |
| * @param isStoreHd {@code true} if this call was used HD. |
| * @param isWifi {@code true} if this call was used wifi. |
| * @param isUsingAssistedDialing {@code true} if this call used assisted dialing. |
| * @param isHdPlus {@code true} if this is call audio quality is HD+ |
| * @param featureFlags Feature flags. |
| * @return The call features. |
| */ |
| private static int getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd, |
| boolean isWifi, boolean isUsingAssistedDialing, boolean isRtt, boolean isVolte, |
| boolean isVonr, boolean isHdPlus, boolean isGroupCall, FeatureFlags featureFlags) { |
| int features = 0; |
| if (VideoProfile.isVideo(videoState)) { |
| features |= Calls.FEATURES_VIDEO; |
| } |
| if (isPulledCall) { |
| features |= Calls.FEATURES_PULLED_EXTERNALLY; |
| } |
| if (isStoreHd) { |
| features |= Calls.FEATURES_HD_CALL; |
| } |
| if (isWifi) { |
| features |= Calls.FEATURES_WIFI; |
| } |
| if (isUsingAssistedDialing) { |
| features |= Calls.FEATURES_ASSISTED_DIALING_USED; |
| } |
| if (isRtt) { |
| features |= Calls.FEATURES_RTT; |
| } |
| if (isVolte) { |
| features |= Calls.FEATURES_VOLTE; |
| } |
| if (featureFlags.hdPlusCall() && isVonr) { |
| features |= Calls.FEATURES_VONR; |
| } |
| if (featureFlags.hdPlusCall() && isHdPlus) { |
| features |= Calls.FEATURES_HD_PLUS_CALL; |
| } |
| if (isGroupCall) { |
| features |= Calls.FEATURES_GROUP_CALL; |
| } |
| |
| return features; |
| } |
| |
| /** |
| * Retrieve the phone number from the call, and then process it before returning the |
| * actual number that is to be logged. |
| * |
| * @param call The phone connection. |
| * @return the phone number to be logged. |
| */ |
| private String getLogNumber(Call call) { |
| Uri handle = call.getOriginalHandle(); |
| |
| if (handle == null) { |
| return null; |
| } |
| |
| String handleString = handle.getSchemeSpecificPart(); |
| String scheme = handle.getScheme(); |
| |
| if (TextUtils.isEmpty(handleString) && (PhoneAccount.SCHEME_VOICEMAIL.equals(scheme))) { |
| // This is a voicemail.Get voicemail number for this voicemail call. |
| final PhoneAccountHandle accountHandle = call.getTargetPhoneAccount(); |
| TelecomManager tm = mContext.getSystemService(TelecomManager.class); |
| if (tm != null) { |
| handleString = tm.getVoiceMailNumber(accountHandle); |
| } |
| } |
| |
| if (!PhoneNumberUtils.isUriNumber(handleString)) { |
| handleString = PhoneNumberUtils.stripSeparators(handleString); |
| } |
| return handleString; |
| } |
| |
| /** |
| * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider |
| * using an AsyncTask to avoid blocking the main thread. |
| * |
| * @param args Prepopulated call details. |
| * @return A handle to the AsyncTask that will add the call to the call log asynchronously. |
| */ |
| public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) { |
| return new LogCallAsyncTask().execute(args); |
| } |
| |
| /** |
| * Helper AsyncTask to access the call logs database asynchronously since database operations |
| * can take a long time depending on the system's load. Since it extends AsyncTask, it uses |
| * its own thread pool. |
| */ |
| private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> { |
| |
| private LogCallCompletedListener[] mListeners; |
| |
| @Override |
| protected Uri[] doInBackground(AddCallArgs... callList) { |
| int count = callList.length; |
| Uri[] result = new Uri[count]; |
| mListeners = new LogCallCompletedListener[count]; |
| for (int i = 0; i < count; i++) { |
| AddCallArgs c = callList[i]; |
| mListeners[i] = c.logCallCompletedListener; |
| try { |
| result[i] = CallLogUtils.addCall(c.context, c.params); |
| Log.i(TAG, "LogCall; logged callId=%s, uri=%s", |
| c.call.getId(), result[i]); |
| if (result[i] == null) { |
| // No call was added or even worse we lost a call in the log. Trigger an |
| // anomaly report. Note: it technically possible that an app modified the |
| // call log while we were writing to it here; that is pretty unlikely, and |
| // the goal here is to try and identify potential anomalous conditions with |
| // logging calls. |
| mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID, |
| LOG_CALL_FAILED_ANOMALY_DESC); |
| } |
| } catch (Exception e) { |
| // This is very rare but may happen in legitimate cases. |
| // E.g. If the phone is encrypted and thus write request fails, it may cause |
| // some kind of Exception (right now it is IllegalArgumentException, but this |
| // might change). |
| // |
| // We don't want to crash the whole process just because of that, so just log |
| // it instead. |
| Log.e(TAG, e, "LogCall: Exception raised adding callId=%s", c.call.getId()); |
| result[i] = null; |
| mAnomalyReporterAdapter.reportAnomaly(LOG_CALL_FAILED_ANOMALY_ID, |
| LOG_CALL_FAILED_ANOMALY_DESC); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| protected void onPostExecute(Uri[] result) { |
| for (int i = 0; i < result.length; i++) { |
| Uri uri = result[i]; |
| /* |
| Performs a simple correctness check to make sure the call was written in the |
| database. |
| Typically there is only one result per call so it is easy to identify which one |
| failed. |
| */ |
| if (uri == null) { |
| Log.w(TAG, "Failed to write call to the log."); |
| } |
| if (mListeners[i] != null) { |
| mListeners[i].onLogCompleted(uri); |
| } |
| } |
| } |
| } |
| |
| private void sendAddCallBroadcast(int callType, long duration) { |
| Intent callAddIntent = new Intent(ACTION_CALLS_TABLE_ADD_ENTRY); |
| callAddIntent.putExtra(CALL_TYPE, callType); |
| callAddIntent.putExtra(CALL_DURATION, duration); |
| mContext.sendBroadcast(callAddIntent, PERMISSION_PROCESS_CALLLOG_INFO); |
| } |
| |
| /** |
| * Get the current country code |
| * |
| * @return the ISO 3166-1 two letters country code of current country. |
| */ |
| public String getCountryIso() { |
| synchronized (mLock) { |
| if (mCurrentCountryIso == null) { |
| // Moving this into the constructor will pose issues if the service is not yet set |
| // up, causing a RemoteException to be thrown. Note that the callback is only |
| // registered if the country iso cache is null (so in an ideal setting, this should |
| // only require a one-time configuration). |
| /*final CountryDetector countryDetector = |
| (CountryDetector) mContext.getSystemService(Context.COUNTRY_DETECTOR); |
| if (countryDetector != null) { |
| countryDetector.registerCountryDetectorCallback( |
| mCountryCodeExecutor, this::countryCodeConsumer); |
| }*/ |
| mCurrentCountryIso = getCurrentCountryIso(mContext); |
| } |
| return mCurrentCountryIso; |
| } |
| } |
| |
| /** |
| * Retrieves the current country ISO code. It first tries to get the network country ISO, |
| * then the SIM country ISO, and finally falls back to the default locale's country. |
| * |
| * @param context The current context. |
| * @return The ISO 3166-1 two-letter country code of the current country, or {@code null} if |
| * it cannot be determined. The returned string is in uppercase. |
| */ |
| private String getCurrentCountryIso(Context context) { |
| String countryIso = null; |
| TelephonyManager tm = context.getSystemService(TelephonyManager.class); |
| try { |
| if (tm != null) { |
| countryIso = tm.getNetworkCountryIso(); |
| if (TextUtils.isEmpty(countryIso)) { |
| countryIso = tm.getSimCountryIso(); |
| } |
| } |
| } catch (UnsupportedOperationException e) { |
| // Telecom can run on devices without FEATURE_TELEPHONY_CALLING, in which case |
| // TelephonyManager methods will throw UnsupportedOperationException. |
| // The Logic falls back to Locale |
| Log.w(TAG, "getCurrentCountryIso: TelephonyManager methods failed: " + e.getMessage()); |
| } |
| if (TextUtils.isEmpty(countryIso)) { |
| countryIso = Locale.getDefault().getCountry(); |
| } |
| return countryIso != null ? countryIso.toUpperCase(Locale.US) : null; |
| } |
| |
| @VisibleForTesting |
| public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){ |
| mAnomalyReporterAdapter = anomalyReporterAdapter; |
| } |
| |
| public static boolean shouldLogVoipCall(Call call) { |
| boolean shouldLogVoipCall = call.isLoggedSelfManaged() || call.isLoggedTransactional(); |
| return (call.isManaged() || (shouldLogVoipCall)) |
| && (call.getHandoverState() == HandoverState.HANDOVER_NONE |
| || call.getHandoverState() == HandoverState.HANDOVER_COMPLETE); |
| } |
| } |