/*
 * 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.AddCallParams.AddCallParametersBuilder.MAX_NUMBER_OF_CHARACTERS;
import static android.provider.CallLog.Calls.BLOCK_REASON_NOT_BLOCKED;
import static android.telephony.CarrierConfigManager.KEY_SUPPORT_IMS_CONFERENCE_EVENT_PACKAGE_BOOL;

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.Country;
import android.location.CountryDetector;
import android.location.Location;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerExecutor;
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.util.Pair;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.callfiltering.CallFilteringResult;
import com.android.server.telecom.flags.FeatureFlags;
import com.android.server.telecom.flags.Flags;

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, CallLog.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 CallLog.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 Country mCurrentCountry;
    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)) {
            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.isSelfManaged();
            logCall(call, type, showNotification, null /*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 = call.isLoggedSelfManaged()
                && (call.getHandoverState() == HandoverState.HANDOVER_NONE
                || call.getHandoverState() == HandoverState.HANDOVER_COMPLETE);

        // "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 (mFeatureFlags.telecomSkipLogBasedOnExtra() && 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)
                || !mFeatureFlags.telecomLogExternalWearableCalls())) {
            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.isSelfManaged() || 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}.
     */
    void logCall(Call call, int callLogType,
            @Nullable LogCallCompletedListener logCallCompletedListener, CallFilteringResult result) {

        CallLog.AddCallParams.AddCallParametersBuilder paramBuilder =
                new CallLog.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()));

        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, mFeatureFlags)) {
                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.setCallerInfo(call.getCallerInfo());
        paramBuilder.setPostDialDigits(call.getPostDialDigits());
        paramBuilder.setPresentation(call.getHandlePresentation());
        paramBuilder.setCallType(callLogType);
        paramBuilder.setIsRead(call.isSelfManaged());
        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);
                }
            }
        }
        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());
            logCallAsync(args);
        } else {
            Log.addEvent(call, LogUtils.Events.SKIP_CALL_LOG);
        }
    }

    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 = mContext.getResources()
                .getStringArray(com.android.internal.R.array.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.
     * @return The call features.
     */
    private static int getCallFeatures(int videoState, boolean isPulledCall, boolean isStoreHd,
            boolean isWifi, boolean isUsingAssistedDialing, boolean isRtt, boolean isVolte) {
        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;
        }
        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();
        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] = Calls.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);
    }

    private String getCountryIsoFromCountry(Country country) {
        if(country == null) {
            // Fallback to Locale if there are issues with CountryDetector
            Log.w(TAG, "Value for country was null. Falling back to Locale.");
            return Locale.getDefault().getCountry();
        }

        return country.getCountryCode();
    }

    /**
     * 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 = getCountryIsoFromCountry(mCurrentCountry);
            }
            return mCurrentCountryIso;
        }
    }

    /** Consumer to receive the country code if it changes. */
    private void countryCodeConsumer(Country newCountry) {
        Log.startSession("CLM.cCC");
        try {
            Log.i(TAG, "Country ISO changed. Retrieving new ISO...");
            synchronized (mLock) {
                mCurrentCountry = newCountry;
                mCurrentCountryIso = getCountryIsoFromCountry(newCountry);
            }
        } finally {
            Log.endSession();
        }
    }

    @VisibleForTesting
    public void setAnomalyReporterAdapter(AnomalyReporterAdapter anomalyReporterAdapter){
        mAnomalyReporterAdapter = anomalyReporterAdapter;
    }
}
