/*
 * Copyright (C) 2016 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.internal.telephony.metrics;

import static android.text.format.DateUtils.MINUTE_IN_MILLIS;

import static com.android.internal.telephony.RILConstants.RIL_REQUEST_ANSWER;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_CDMA_SEND_SMS;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_DEACTIVATE_DATA_CALL;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_DIAL;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_IMS_SEND_SMS;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SEND_SMS;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SEND_SMS_EXPECT_MORE;
import static com.android.internal.telephony.RILConstants.RIL_REQUEST_SETUP_DATA_CALL;
import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_IP;
import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_IPV4V6;
import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_IPV6;
import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_NON_IP;
import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_PPP;
import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_TYPE_UNSTRUCTURED;
import static com.android.internal.telephony.nano.TelephonyProto.PdpType.PDP_UNKNOWN;

import android.content.Context;
import android.net.NetworkCapabilities;
import android.os.BatteryStatsManager;
import android.os.Build;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.provider.Telephony.Sms.Intents;
import android.telephony.AccessNetworkConstants;
import android.telephony.CallQuality;
import android.telephony.DisconnectCause;
import android.telephony.NetworkRegistrationInfo;
import android.telephony.ServiceState;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyHistogram;
import android.telephony.TelephonyManager;
import android.telephony.data.DataCallResponse;
import android.telephony.data.DataService;
import android.telephony.emergency.EmergencyNumber;
import android.telephony.ims.ImsCallProfile;
import android.telephony.ims.ImsCallSession;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.ImsStreamMediaProfile;
import android.telephony.ims.feature.MmTelFeature;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.telephony.ims.stub.ImsSmsImplBase;
import android.text.TextUtils;
import android.util.Base64;
import android.util.SparseArray;

import com.android.internal.telephony.CarrierResolver;
import com.android.internal.telephony.DriverCall;
import com.android.internal.telephony.GsmCdmaConnection;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.RIL;
import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.SmsResponse;
import com.android.internal.telephony.UUSInfo;
import com.android.internal.telephony.emergency.EmergencyNumberTracker;
import com.android.internal.telephony.imsphone.ImsPhoneCall;
import com.android.internal.telephony.nano.TelephonyProto;
import com.android.internal.telephony.nano.TelephonyProto.ActiveSubscriptionInfo;
import com.android.internal.telephony.nano.TelephonyProto.EmergencyNumberInfo;
import com.android.internal.telephony.nano.TelephonyProto.ImsCapabilities;
import com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
import com.android.internal.telephony.nano.TelephonyProto.ModemPowerStats;
import com.android.internal.telephony.nano.TelephonyProto.RilDataCall;
import com.android.internal.telephony.nano.TelephonyProto.SimState;
import com.android.internal.telephony.nano.TelephonyProto.SmsSession;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.CallState;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.RilCall;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.RilCall.Type;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierIdMatching;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierIdMatchingResult;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierKeyChange;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.DataSwitch;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.ModemRestart;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.NetworkCapabilitiesInfo;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.OnDemandDataSwitch;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilDeactivateDataCall;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilDeactivateDataCall.DeactivateReason;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCall;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCallResponse;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCallResponse.RilDataCallFailCause;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyLog;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyServiceState;
import com.android.internal.telephony.nano.TelephonyProto.TelephonySettings;
import com.android.internal.telephony.nano.TelephonyProto.TimeInterval;
import com.android.internal.telephony.protobuf.nano.MessageNano;
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.telephony.Rlog;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

/**
 * Telephony metrics holds all metrics events and convert it into telephony proto buf.
 * @hide
 */
public class TelephonyMetrics {

    private static final String TAG = TelephonyMetrics.class.getSimpleName();

    private static final boolean DBG = true;
    private static final boolean VDBG = false; // STOPSHIP if true

    /** Maximum telephony events stored */
    private static final int MAX_TELEPHONY_EVENTS = 1000;

    /** Maximum call sessions stored */
    private static final int MAX_COMPLETED_CALL_SESSIONS = 50;

    /** Maximum sms sessions stored */
    private static final int MAX_COMPLETED_SMS_SESSIONS = 500;

    /** For reducing the timing precision for privacy purposes */
    private static final int SESSION_START_PRECISION_MINUTES = 5;

    /** The TelephonyMetrics singleton instance */
    private static TelephonyMetrics sInstance;

    /** Telephony events */
    private final Deque<TelephonyEvent> mTelephonyEvents = new ArrayDeque<>();

    /**
     * In progress call sessions. Note that each phone can only have up to 1 in progress call
     * session (might contains multiple calls). Having a sparse array in case we need to support
     * DSDA in the future.
     */
    private final SparseArray<InProgressCallSession> mInProgressCallSessions = new SparseArray<>();

    /** The completed call sessions */
    private final Deque<TelephonyCallSession> mCompletedCallSessions = new ArrayDeque<>();

    /** The in-progress SMS sessions. When finished, it will be moved into the completed sessions */
    private final SparseArray<InProgressSmsSession> mInProgressSmsSessions = new SparseArray<>();

    /** The completed SMS sessions */
    private final Deque<SmsSession> mCompletedSmsSessions = new ArrayDeque<>();

    /** Last service state. This is for injecting the base of a new log or a new call/sms session */
    private final SparseArray<TelephonyServiceState> mLastServiceState = new SparseArray<>();

    /**
     * Last ims capabilities. This is for injecting the base of a new log or a new call/sms
     * session
     */
    private final SparseArray<ImsCapabilities> mLastImsCapabilities = new SparseArray<>();

    /**
     * Last IMS connection state. This is for injecting the base of a new log or a new call/sms
     * session
     */
    private final SparseArray<ImsConnectionState> mLastImsConnectionState = new SparseArray<>();

    /**
     * Last settings state. This is for deduping same settings event logged.
     */
    private final SparseArray<TelephonySettings> mLastSettings = new SparseArray<>();

    /**
     * Last sim state, indexed by phone id.
     */
    private final SparseArray<Integer> mLastSimState = new SparseArray<>();

    /**
     * Last active subscription information, indexed by phone id.
     */
    private final SparseArray<ActiveSubscriptionInfo> mLastActiveSubscriptionInfos =
            new SparseArray<>();

    /**
     * The last modem state represent by a bitmap, the i-th bit(LSB) indicates the i-th modem
     * state(0 - disabled, 1 - enabled).
     *
     * TODO: initialize the enabled modem bitmap when it's possible to get the modem state.
     */
    private int mLastEnabledModemBitmap = (1 << TelephonyManager.getDefault().getPhoneCount()) - 1;

    /**
     * Last carrier id matching.
     */
    private final SparseArray<CarrierIdMatching> mLastCarrierId = new SparseArray<>();

    /**
     * Last NetworkCapabilitiesInfo, indexed by phone id.
     */
    private final SparseArray<NetworkCapabilitiesInfo> mLastNetworkCapabilitiesInfos =
            new SparseArray<>();

    /**
     * Last RilDataCall Events (indexed by cid), indexed by phone id
     */
    private final SparseArray<SparseArray<RilDataCall>> mLastRilDataCallEvents =
            new SparseArray<>();

    /** The start system time of the TelephonyLog in milliseconds*/
    private long mStartSystemTimeMs;

    /** The start elapsed time of the TelephonyLog in milliseconds*/
    private long mStartElapsedTimeMs;

    /** Indicating if some of the telephony events are dropped in this log */
    private boolean mTelephonyEventsDropped = false;

    private Context mContext;

    public TelephonyMetrics() {
        mStartSystemTimeMs = System.currentTimeMillis();
        mStartElapsedTimeMs = SystemClock.elapsedRealtime();
    }

    /**
     * Get the singleton instance of telephony metrics.
     *
     * @return The instance
     */
    public synchronized static TelephonyMetrics getInstance() {
        if (sInstance == null) {
            sInstance = new TelephonyMetrics();
        }

        return sInstance;
    }

    /**
     * Set the context for telephony metrics.
     *
     * @param context Context
     * @hide
     */
    public void setContext(Context context) {
        mContext = context;
    }

    /**
     * Dump the state of various objects, add calls to other objects as desired.
     *
     * @param fd File descriptor
     * @param pw Print writer
     * @param args Arguments
     */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        if (args != null && args.length > 0) {
            boolean reset = true;
            if (args.length > 1 && "--keep".equals(args[1])) {
                reset = false;
            }

            switch (args[0]) {
                case "--metrics":
                    printAllMetrics(pw);
                    break;
                case "--metricsproto":
                    pw.println(convertProtoToBase64String(buildProto()));
                    if (reset) {
                        reset();
                    }
                    break;
                case "--metricsprototext":
                    pw.println(buildProto().toString());
                    break;
            }
        }
    }

    private void logv(String log) {
        if (VDBG) {
            Rlog.v(TAG, log);
        }
    }

    /**
     * Convert the telephony event to string
     *
     * @param event The event in integer
     * @return The event in string
     */
    private static String telephonyEventToString(int event) {
        switch (event) {
            case TelephonyEvent.Type.UNKNOWN:
                return "UNKNOWN";
            case TelephonyEvent.Type.SETTINGS_CHANGED:
                return "SETTINGS_CHANGED";
            case TelephonyEvent.Type.RIL_SERVICE_STATE_CHANGED:
                return "RIL_SERVICE_STATE_CHANGED";
            case TelephonyEvent.Type.IMS_CONNECTION_STATE_CHANGED:
                return "IMS_CONNECTION_STATE_CHANGED";
            case TelephonyEvent.Type.IMS_CAPABILITIES_CHANGED:
                return "IMS_CAPABILITIES_CHANGED";
            case TelephonyEvent.Type.DATA_CALL_SETUP:
                return "DATA_CALL_SETUP";
            case TelephonyEvent.Type.DATA_CALL_SETUP_RESPONSE:
                return "DATA_CALL_SETUP_RESPONSE";
            case TelephonyEvent.Type.DATA_CALL_LIST_CHANGED:
                return "DATA_CALL_LIST_CHANGED";
            case TelephonyEvent.Type.DATA_CALL_DEACTIVATE:
                return "DATA_CALL_DEACTIVATE";
            case TelephonyEvent.Type.DATA_CALL_DEACTIVATE_RESPONSE:
                return "DATA_CALL_DEACTIVATE_RESPONSE";
            case TelephonyEvent.Type.DATA_STALL_ACTION:
                return "DATA_STALL_ACTION";
            case TelephonyEvent.Type.MODEM_RESTART:
                return "MODEM_RESTART";
            case TelephonyEvent.Type.CARRIER_ID_MATCHING:
                return "CARRIER_ID_MATCHING";
            case TelephonyEvent.Type.NITZ_TIME:
                return "NITZ_TIME";
            case TelephonyEvent.Type.EMERGENCY_NUMBER_REPORT:
                return "EMERGENCY_NUMBER_REPORT";
            case TelephonyEvent.Type.NETWORK_CAPABILITIES_CHANGED:
                return "NETWORK_CAPABILITIES_CHANGED";
            default:
                return Integer.toString(event);
        }
    }

    /**
     * Convert the call session event into string
     *
     * @param event The event in integer
     * @return The event in String
     */
    private static String callSessionEventToString(int event) {
        switch (event) {
            case TelephonyCallSession.Event.Type.EVENT_UNKNOWN:
                return "EVENT_UNKNOWN";
            case TelephonyCallSession.Event.Type.SETTINGS_CHANGED:
                return "SETTINGS_CHANGED";
            case TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED:
                return "RIL_SERVICE_STATE_CHANGED";
            case TelephonyCallSession.Event.Type.IMS_CONNECTION_STATE_CHANGED:
                return "IMS_CONNECTION_STATE_CHANGED";
            case TelephonyCallSession.Event.Type.IMS_CAPABILITIES_CHANGED:
                return "IMS_CAPABILITIES_CHANGED";
            case TelephonyCallSession.Event.Type.DATA_CALL_LIST_CHANGED:
                return "DATA_CALL_LIST_CHANGED";
            case TelephonyCallSession.Event.Type.RIL_REQUEST:
                return "RIL_REQUEST";
            case TelephonyCallSession.Event.Type.RIL_RESPONSE:
                return "RIL_RESPONSE";
            case TelephonyCallSession.Event.Type.RIL_CALL_RING:
                return "RIL_CALL_RING";
            case TelephonyCallSession.Event.Type.RIL_CALL_SRVCC:
                return "RIL_CALL_SRVCC";
            case TelephonyCallSession.Event.Type.RIL_CALL_LIST_CHANGED:
                return "RIL_CALL_LIST_CHANGED";
            case TelephonyCallSession.Event.Type.IMS_COMMAND:
                return "IMS_COMMAND";
            case TelephonyCallSession.Event.Type.IMS_COMMAND_RECEIVED:
                return "IMS_COMMAND_RECEIVED";
            case TelephonyCallSession.Event.Type.IMS_COMMAND_FAILED:
                return "IMS_COMMAND_FAILED";
            case TelephonyCallSession.Event.Type.IMS_COMMAND_COMPLETE:
                return "IMS_COMMAND_COMPLETE";
            case TelephonyCallSession.Event.Type.IMS_CALL_RECEIVE:
                return "IMS_CALL_RECEIVE";
            case TelephonyCallSession.Event.Type.IMS_CALL_STATE_CHANGED:
                return "IMS_CALL_STATE_CHANGED";
            case TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED:
                return "IMS_CALL_TERMINATED";
            case TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER:
                return "IMS_CALL_HANDOVER";
            case TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER_FAILED:
                return "IMS_CALL_HANDOVER_FAILED";
            case TelephonyCallSession.Event.Type.PHONE_STATE_CHANGED:
                return "PHONE_STATE_CHANGED";
            case TelephonyCallSession.Event.Type.NITZ_TIME:
                return "NITZ_TIME";
            case TelephonyCallSession.Event.Type.AUDIO_CODEC:
                return "AUDIO_CODEC";
            default:
                return Integer.toString(event);
        }
    }

    /**
     * Convert the SMS session event into string
     * @param event The event in integer
     * @return The event in String
     */
    private static String smsSessionEventToString(int event) {
        switch (event) {
            case SmsSession.Event.Type.EVENT_UNKNOWN:
                return "EVENT_UNKNOWN";
            case SmsSession.Event.Type.SETTINGS_CHANGED:
                return "SETTINGS_CHANGED";
            case SmsSession.Event.Type.RIL_SERVICE_STATE_CHANGED:
                return "RIL_SERVICE_STATE_CHANGED";
            case SmsSession.Event.Type.IMS_CONNECTION_STATE_CHANGED:
                return "IMS_CONNECTION_STATE_CHANGED";
            case SmsSession.Event.Type.IMS_CAPABILITIES_CHANGED:
                return "IMS_CAPABILITIES_CHANGED";
            case SmsSession.Event.Type.DATA_CALL_LIST_CHANGED:
                return "DATA_CALL_LIST_CHANGED";
            case SmsSession.Event.Type.SMS_SEND:
                return "SMS_SEND";
            case SmsSession.Event.Type.SMS_SEND_RESULT:
                return "SMS_SEND_RESULT";
            case SmsSession.Event.Type.SMS_RECEIVED:
                return "SMS_RECEIVED";
            case SmsSession.Event.Type.INCOMPLETE_SMS_RECEIVED:
                return "INCOMPLETE_SMS_RECEIVED";
            default:
                return Integer.toString(event);
        }
    }

    /**
     * Print all metrics data for debugging purposes
     *
     * @param rawWriter Print writer
     */
    private synchronized void printAllMetrics(PrintWriter rawWriter) {
        final IndentingPrintWriter pw = new IndentingPrintWriter(rawWriter, "  ");

        pw.println("Telephony metrics proto:");
        pw.println("------------------------------------------");
        pw.println("Telephony events:");
        pw.increaseIndent();
        for (TelephonyEvent event : mTelephonyEvents) {
            pw.print(event.timestampMillis);
            pw.print(" [");
            pw.print(event.phoneId);
            pw.print("] ");

            pw.print("T=");
            if (event.type == TelephonyEvent.Type.RIL_SERVICE_STATE_CHANGED) {
                pw.print(telephonyEventToString(event.type)
                        + "(" + "Data RAT " + event.serviceState.dataRat
                        + " Voice RAT " + event.serviceState.voiceRat
                        + " Channel Number " + event.serviceState.channelNumber
                        + " NR Frequency Range " + event.serviceState.nrFrequencyRange
                        + " NR State " + event.serviceState.nrState
                        + ")");
                for (int i = 0; i < event.serviceState.networkRegistrationInfo.length; i++) {
                    pw.print("reg info: domain="
                            + event.serviceState.networkRegistrationInfo[i].domain
                            + ", rat=" + event.serviceState.networkRegistrationInfo[i].rat);
                }
            } else {
                pw.print(telephonyEventToString(event.type));
            }

            pw.println("");
        }

        pw.decreaseIndent();
        pw.println("Call sessions:");
        pw.increaseIndent();

        for (TelephonyCallSession callSession : mCompletedCallSessions) {
            pw.print("Start time in minutes: " + callSession.startTimeMinutes);
            pw.print(", phone: " + callSession.phoneId);
            if (callSession.eventsDropped) {
                pw.println(" Events dropped: " + callSession.eventsDropped);
            }

            pw.println(" Events: ");
            pw.increaseIndent();
            for (TelephonyCallSession.Event event : callSession.events) {
                pw.print(event.delay);
                pw.print(" T=");
                if (event.type == TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED) {
                    pw.println(callSessionEventToString(event.type)
                            + "(" + "Data RAT " + event.serviceState.dataRat
                            + " Voice RAT " + event.serviceState.voiceRat
                            + " Channel Number " + event.serviceState.channelNumber
                            + " NR Frequency Range " + event.serviceState.nrFrequencyRange
                            + " NR State " + event.serviceState.nrState
                            + ")");
                } else if (event.type == TelephonyCallSession.Event.Type.RIL_CALL_LIST_CHANGED) {
                    pw.println(callSessionEventToString(event.type));
                    pw.increaseIndent();
                    for (RilCall call : event.calls) {
                        pw.println(call.index + ". Type = " + call.type + " State = "
                                + call.state + " End Reason " + call.callEndReason
                                + " Precise Disconnect Cause " + call.preciseDisconnectCause
                                + " isMultiparty = " + call.isMultiparty);
                    }
                    pw.decreaseIndent();
                } else if (event.type == TelephonyCallSession.Event.Type.AUDIO_CODEC) {
                    pw.println(callSessionEventToString(event.type)
                            + "(" + event.audioCodec + ")");
                } else {
                    pw.println(callSessionEventToString(event.type));
                }
            }
            pw.decreaseIndent();
        }

        pw.decreaseIndent();
        pw.println("Sms sessions:");
        pw.increaseIndent();

        int count = 0;
        for (SmsSession smsSession : mCompletedSmsSessions) {
            count++;
            pw.print("[" + count + "] Start time in minutes: "
                    + smsSession.startTimeMinutes);
            pw.print(", phone: " + smsSession.phoneId);
            if (smsSession.eventsDropped) {
                pw.println(", events dropped: " + smsSession.eventsDropped);
            } else {
                pw.println("");
            }
            pw.println("Events: ");
            pw.increaseIndent();
            for (SmsSession.Event event : smsSession.events) {
                pw.print(event.delay);
                pw.print(" T=");
                pw.println(smsSessionEventToString(event.type));
                // Only show more info for tx/rx sms
                if (event.type == SmsSession.Event.Type.SMS_RECEIVED) {
                    pw.increaseIndent();
                    switch (event.smsType) {
                        case SmsSession.Event.SmsType.SMS_TYPE_SMS_PP:
                            pw.println("Type: SMS-PP");
                            break;
                        case SmsSession.Event.SmsType.SMS_TYPE_VOICEMAIL_INDICATION:
                            pw.println("Type: Voicemail indication");
                            break;
                        case SmsSession.Event.SmsType.SMS_TYPE_ZERO:
                            pw.println("Type: zero");
                            break;
                        case SmsSession.Event.SmsType.SMS_TYPE_WAP_PUSH:
                            pw.println("Type: WAP PUSH");
                            break;
                        default:
                            break;
                    }
                    if (event.errorCode != SmsManager.RESULT_ERROR_NONE) {
                        pw.println("E=" + event.errorCode);
                    }
                    pw.decreaseIndent();
                } else if (event.type == SmsSession.Event.Type.SMS_SEND
                        || event.type == SmsSession.Event.Type.SMS_SEND_RESULT) {
                    pw.increaseIndent();
                    pw.println("ReqId=" + event.rilRequestId);
                    pw.println("E=" + event.errorCode);
                    pw.println("RilE=" + event.error);
                    pw.println("ImsE=" + event.imsError);
                    pw.decreaseIndent();
                } else if (event.type == SmsSession.Event.Type.INCOMPLETE_SMS_RECEIVED) {
                    pw.increaseIndent();
                    pw.println("Received: " + event.incompleteSms.receivedParts + "/"
                            + event.incompleteSms.totalParts);
                    pw.decreaseIndent();
                }
            }
            pw.decreaseIndent();
        }

        pw.decreaseIndent();
        pw.println("Modem power stats:");
        pw.increaseIndent();

        BatteryStatsManager batteryStatsManager = mContext == null ? null :
                (BatteryStatsManager) mContext.getSystemService(Context.BATTERY_STATS_SERVICE);
        ModemPowerStats s = new ModemPowerMetrics(batteryStatsManager).buildProto();

        pw.println("Power log duration (battery time) (ms): " + s.loggingDurationMs);
        pw.println("Energy consumed by modem (mAh): " + s.energyConsumedMah);
        pw.println("Number of packets sent (tx): " + s.numPacketsTx);
        pw.println("Number of bytes sent (tx): " + s.numBytesTx);
        pw.println("Number of packets received (rx): " + s.numPacketsRx);
        pw.println("Number of bytes received (rx): " + s.numBytesRx);
        pw.println("Amount of time kernel is active because of cellular data (ms): "
                + s.cellularKernelActiveTimeMs);
        pw.println("Amount of time spent in very poor rx signal level (ms): "
                + s.timeInVeryPoorRxSignalLevelMs);
        pw.println("Amount of time modem is in sleep (ms): " + s.sleepTimeMs);
        pw.println("Amount of time modem is in idle (ms): " + s.idleTimeMs);
        pw.println("Amount of time modem is in rx (ms): " + s.rxTimeMs);
        pw.println("Amount of time modem is in tx (ms): " + Arrays.toString(s.txTimeMs));
        pw.println("Amount of time phone spent in various Radio Access Technologies (ms): "
                + Arrays.toString(s.timeInRatMs));
        pw.println("Amount of time phone spent in various cellular "
                + "rx signal strength levels (ms): "
                + Arrays.toString(s.timeInRxSignalStrengthLevelMs));
        pw.println("Energy consumed across measured modem rails (mAh): "
                + new DecimalFormat("#.##").format(s.monitoredRailEnergyConsumedMah));
        pw.decreaseIndent();
        pw.println("Hardware Version: " + SystemProperties.get("ro.boot.revision", ""));
    }

    /**
     * Convert the telephony proto into Base-64 encoded string
     *
     * @param proto Telephony proto
     * @return Encoded string
     */
    private static String convertProtoToBase64String(TelephonyLog proto) {
        return Base64.encodeToString(
                TelephonyProto.TelephonyLog.toByteArray(proto), Base64.DEFAULT);
    }

    /**
     * Reset all events and sessions
     */
    private synchronized void reset() {
        mTelephonyEvents.clear();
        mCompletedCallSessions.clear();
        mCompletedSmsSessions.clear();

        mTelephonyEventsDropped = false;

        mStartSystemTimeMs = System.currentTimeMillis();
        mStartElapsedTimeMs = SystemClock.elapsedRealtime();

        // Insert the last known sim state, enabled modem bitmap, active subscription info,
        // service state, ims capabilities, ims connection states, carrier id and Data call
        // events as the base.
        // Sim state, modem bitmap and active subscription info events are logged before
        // other events.
        addTelephonyEvent(new TelephonyEventBuilder(mStartElapsedTimeMs, -1 /* phoneId */)
                .setSimStateChange(mLastSimState).build());

        addTelephonyEvent(new TelephonyEventBuilder(mStartElapsedTimeMs, -1 /* phoneId */)
                .setEnabledModemBitmap(mLastEnabledModemBitmap).build());

        for (int i = 0; i < mLastActiveSubscriptionInfos.size(); i++) {
          final int key = mLastActiveSubscriptionInfos.keyAt(i);
          TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
                  .setActiveSubscriptionInfoChange(mLastActiveSubscriptionInfos.get(key)).build();
          addTelephonyEvent(event);
        }

        for (int i = 0; i < mLastServiceState.size(); i++) {
            final int key = mLastServiceState.keyAt(i);

            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
                    .setServiceState(mLastServiceState.get(key)).build();
            addTelephonyEvent(event);
        }

        for (int i = 0; i < mLastImsCapabilities.size(); i++) {
            final int key = mLastImsCapabilities.keyAt(i);

            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
                    .setImsCapabilities(mLastImsCapabilities.get(key)).build();
            addTelephonyEvent(event);
        }

        for (int i = 0; i < mLastImsConnectionState.size(); i++) {
            final int key = mLastImsConnectionState.keyAt(i);

            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
                    .setImsConnectionState(mLastImsConnectionState.get(key)).build();
            addTelephonyEvent(event);
        }

        for (int i = 0; i < mLastCarrierId.size(); i++) {
            final int key = mLastCarrierId.keyAt(i);
            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
                    .setCarrierIdMatching(mLastCarrierId.get(key)).build();
            addTelephonyEvent(event);
        }

        for (int i = 0; i < mLastNetworkCapabilitiesInfos.size(); i++) {
            final int key = mLastNetworkCapabilitiesInfos.keyAt(i);
            TelephonyEvent event = new TelephonyEventBuilder(mStartElapsedTimeMs, key)
                    .setNetworkCapabilities(mLastNetworkCapabilitiesInfos.get(key)).build();
            addTelephonyEvent(event);
        }

        for (int i = 0; i < mLastRilDataCallEvents.size(); i++) {
            final int key = mLastRilDataCallEvents.keyAt(i);
            for (int j = 0; j < mLastRilDataCallEvents.get(key).size(); j++) {
                final int cidKey = mLastRilDataCallEvents.get(key).keyAt(j);
                RilDataCall[] dataCalls = new RilDataCall[1];
                dataCalls[0] = mLastRilDataCallEvents.get(key).get(cidKey);
                addTelephonyEvent(new TelephonyEventBuilder(mStartElapsedTimeMs, key)
                        .setDataCalls(dataCalls).build());
            }
        }
    }

    /**
     * Build the telephony proto
     *
     * @return Telephony proto
     */
    private synchronized TelephonyLog buildProto() {

        TelephonyLog log = new TelephonyLog();
        // Build telephony events
        log.events = new TelephonyEvent[mTelephonyEvents.size()];
        mTelephonyEvents.toArray(log.events);
        log.eventsDropped = mTelephonyEventsDropped;

        // Build call sessions
        log.callSessions = new TelephonyCallSession[mCompletedCallSessions.size()];
        mCompletedCallSessions.toArray(log.callSessions);

        // Build SMS sessions
        log.smsSessions = new SmsSession[mCompletedSmsSessions.size()];
        mCompletedSmsSessions.toArray(log.smsSessions);

        // Build histogram. Currently we only support RIL histograms.
        List<TelephonyHistogram> rilHistograms = RIL.getTelephonyRILTimingHistograms();
        log.histograms = new TelephonyProto.TelephonyHistogram[rilHistograms.size()];
        for (int i = 0; i < rilHistograms.size(); i++) {
            log.histograms[i] = new TelephonyProto.TelephonyHistogram();
            TelephonyHistogram rilHistogram = rilHistograms.get(i);
            TelephonyProto.TelephonyHistogram histogramProto = log.histograms[i];

            histogramProto.category = rilHistogram.getCategory();
            histogramProto.id = rilHistogram.getId();
            histogramProto.minTimeMillis = rilHistogram.getMinTime();
            histogramProto.maxTimeMillis = rilHistogram.getMaxTime();
            histogramProto.avgTimeMillis = rilHistogram.getAverageTime();
            histogramProto.count = rilHistogram.getSampleCount();
            histogramProto.bucketCount = rilHistogram.getBucketCount();
            histogramProto.bucketEndPoints = rilHistogram.getBucketEndPoints();
            histogramProto.bucketCounters = rilHistogram.getBucketCounters();
        }

        // Build modem power metrics
        BatteryStatsManager batteryStatsManager = mContext == null ? null :
                (BatteryStatsManager) mContext.getSystemService(Context.BATTERY_STATS_SERVICE);
        log.modemPowerStats = new ModemPowerMetrics(batteryStatsManager).buildProto();

        // Log the hardware revision
        log.hardwareRevision = SystemProperties.get("ro.boot.revision", "");

        // Log the starting system time
        log.startTime = new TelephonyProto.Time();
        log.startTime.systemTimestampMillis = mStartSystemTimeMs;
        log.startTime.elapsedTimestampMillis = mStartElapsedTimeMs;

        log.endTime = new TelephonyProto.Time();
        log.endTime.systemTimestampMillis = System.currentTimeMillis();
        log.endTime.elapsedTimestampMillis = SystemClock.elapsedRealtime();

        // Log the last active subscription information.
        int phoneCount = TelephonyManager.getDefault().getPhoneCount();
        ActiveSubscriptionInfo[] activeSubscriptionInfo =
                new ActiveSubscriptionInfo[phoneCount];
        for (int i = 0; i < mLastActiveSubscriptionInfos.size(); i++) {
            int key = mLastActiveSubscriptionInfos.keyAt(i);
            activeSubscriptionInfo[key] = mLastActiveSubscriptionInfos.get(key);
        }
        for (int i = 0; i < phoneCount; i++) {
            if (activeSubscriptionInfo[i] == null) {
                activeSubscriptionInfo[i] = makeInvalidSubscriptionInfo(i);
            }
        }
        log.lastActiveSubscriptionInfo = activeSubscriptionInfo;

        return log;
    }

    /** Update the sim state. */
    public void updateSimState(int phoneId, int simState) {
        int state = mapSimStateToProto(simState);
        Integer lastSimState = mLastSimState.get(phoneId);
        if (lastSimState == null || !lastSimState.equals(state)) {
            mLastSimState.put(phoneId, state);
            addTelephonyEvent(new TelephonyEventBuilder().setSimStateChange(mLastSimState).build());
        }
    }

    /** Update active subscription info list. */
    public synchronized void updateActiveSubscriptionInfoList(List<SubscriptionInfo> subInfos) {
        List<Integer> inActivePhoneList = new ArrayList<>();
        for (int i = 0; i < mLastActiveSubscriptionInfos.size(); i++) {
            inActivePhoneList.add(mLastActiveSubscriptionInfos.keyAt(i));
        }

        for (SubscriptionInfo info : subInfos) {
            int phoneId = info.getSimSlotIndex();
            inActivePhoneList.removeIf(value -> value.equals(phoneId));
            ActiveSubscriptionInfo activeSubscriptionInfo = new ActiveSubscriptionInfo();
            activeSubscriptionInfo.slotIndex = phoneId;
            activeSubscriptionInfo.isOpportunistic = info.isOpportunistic() ? 1 : 0;
            activeSubscriptionInfo.carrierId = info.getCarrierId();
            if (info.getMccString() != null && info.getMncString() != null) {
                activeSubscriptionInfo.simMccmnc = info.getMccString() + info.getMncString();
            }
            if (!MessageNano.messageNanoEquals(
                    mLastActiveSubscriptionInfos.get(phoneId), activeSubscriptionInfo)) {
                addTelephonyEvent(new TelephonyEventBuilder(phoneId)
                        .setActiveSubscriptionInfoChange(activeSubscriptionInfo).build());

                mLastActiveSubscriptionInfos.put(phoneId, activeSubscriptionInfo);
            }
        }

        for (int phoneId : inActivePhoneList) {
            mLastActiveSubscriptionInfos.remove(phoneId);
            addTelephonyEvent(new TelephonyEventBuilder(phoneId)
                    .setActiveSubscriptionInfoChange(makeInvalidSubscriptionInfo(phoneId)).build());
        }
    }

    /** Update the enabled modem bitmap. */
    public void updateEnabledModemBitmap(int enabledModemBitmap) {
        if (mLastEnabledModemBitmap == enabledModemBitmap) return;
        mLastEnabledModemBitmap = enabledModemBitmap;
        addTelephonyEvent(new TelephonyEventBuilder()
                .setEnabledModemBitmap(mLastEnabledModemBitmap).build());
    }

    private static ActiveSubscriptionInfo makeInvalidSubscriptionInfo(int phoneId) {
        ActiveSubscriptionInfo invalidSubscriptionInfo = new ActiveSubscriptionInfo();
        invalidSubscriptionInfo.slotIndex = phoneId;
        invalidSubscriptionInfo.carrierId = -1;
        invalidSubscriptionInfo.isOpportunistic = -1;
        return invalidSubscriptionInfo;
    }

    /**
     * Reduce precision to meet privacy requirements.
     *
     * @param timestamp timestamp in milliseconds
     * @return Precision reduced timestamp in minutes
     */
    static int roundSessionStart(long timestamp) {
        return (int) ((timestamp) / (MINUTE_IN_MILLIS * SESSION_START_PRECISION_MINUTES)
                * (SESSION_START_PRECISION_MINUTES));
    }

    /**
     * Write the Carrier Key change event
     *
     * @param phoneId Phone id
     * @param keyType type of key
     * @param isDownloadSuccessful true if the key was successfully downloaded
     */
    public void writeCarrierKeyEvent(int phoneId, int keyType,  boolean isDownloadSuccessful) {
        final CarrierKeyChange carrierKeyChange = new CarrierKeyChange();
        carrierKeyChange.keyType = keyType;
        carrierKeyChange.isDownloadSuccessful = isDownloadSuccessful;
        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setCarrierKeyChange(
                carrierKeyChange).build();
        addTelephonyEvent(event);
    }


    /**
     * Get the time interval with reduced prevision
     *
     * @param previousTimestamp Previous timestamp in milliseconds
     * @param currentTimestamp Current timestamp in milliseconds
     * @return The time interval
     */
    static int toPrivacyFuzzedTimeInterval(long previousTimestamp, long currentTimestamp) {
        long diff = currentTimestamp - previousTimestamp;
        if (diff < 0) {
            return TimeInterval.TI_UNKNOWN;
        } else if (diff <= 10) {
            return TimeInterval.TI_10_MILLIS;
        } else if (diff <= 20) {
            return TimeInterval.TI_20_MILLIS;
        } else if (diff <= 50) {
            return TimeInterval.TI_50_MILLIS;
        } else if (diff <= 100) {
            return TimeInterval.TI_100_MILLIS;
        } else if (diff <= 200) {
            return TimeInterval.TI_200_MILLIS;
        } else if (diff <= 500) {
            return TimeInterval.TI_500_MILLIS;
        } else if (diff <= 1000) {
            return TimeInterval.TI_1_SEC;
        } else if (diff <= 2000) {
            return TimeInterval.TI_2_SEC;
        } else if (diff <= 5000) {
            return TimeInterval.TI_5_SEC;
        } else if (diff <= 10000) {
            return TimeInterval.TI_10_SEC;
        } else if (diff <= 30000) {
            return TimeInterval.TI_30_SEC;
        } else if (diff <= 60000) {
            return TimeInterval.TI_1_MINUTE;
        } else if (diff <= 180000) {
            return TimeInterval.TI_3_MINUTES;
        } else if (diff <= 600000) {
            return TimeInterval.TI_10_MINUTES;
        } else if (diff <= 1800000) {
            return TimeInterval.TI_30_MINUTES;
        } else if (diff <= 3600000) {
            return TimeInterval.TI_1_HOUR;
        } else if (diff <= 7200000) {
            return TimeInterval.TI_2_HOURS;
        } else if (diff <= 14400000) {
            return TimeInterval.TI_4_HOURS;
        } else {
            return TimeInterval.TI_MANY_HOURS;
        }
    }

    /**
     * Convert the service state into service state proto
     *
     * @param serviceState Service state
     * @return Service state proto
     */
    private TelephonyServiceState toServiceStateProto(ServiceState serviceState) {
        TelephonyServiceState ssProto = new TelephonyServiceState();

        ssProto.voiceRoamingType = serviceState.getVoiceRoamingType();
        ssProto.dataRoamingType = serviceState.getDataRoamingType();

        ssProto.voiceOperator = new TelephonyServiceState.TelephonyOperator();
        ssProto.dataOperator = new TelephonyServiceState.TelephonyOperator();
        if (serviceState.getOperatorAlphaLong() != null) {
            ssProto.voiceOperator.alphaLong = serviceState.getOperatorAlphaLong();
            ssProto.dataOperator.alphaLong = serviceState.getOperatorAlphaLong();
        }

        if (serviceState.getOperatorAlphaShort() != null) {
            ssProto.voiceOperator.alphaShort = serviceState.getOperatorAlphaShort();
            ssProto.dataOperator.alphaShort = serviceState.getOperatorAlphaShort();
        }

        if (serviceState.getOperatorNumeric() != null) {
            ssProto.voiceOperator.numeric = serviceState.getOperatorNumeric();
            ssProto.dataOperator.numeric = serviceState.getOperatorNumeric();
        }

        // Log PS WWAN only because CS WWAN would be exactly the same as voiceRat, and PS WLAN
        // would be always IWLAN in the rat field.
        // Note that we intentionally do not log reg state because it changes too frequently that
        // will grow the proto size too much.
        List<TelephonyServiceState.NetworkRegistrationInfo> nriList = new ArrayList<>();
        NetworkRegistrationInfo nri = serviceState.getNetworkRegistrationInfo(
                NetworkRegistrationInfo.DOMAIN_PS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN);
        if (nri != null) {
            TelephonyServiceState.NetworkRegistrationInfo nriProto =
                    new TelephonyServiceState.NetworkRegistrationInfo();
            nriProto.domain = TelephonyServiceState.Domain.DOMAIN_PS;
            nriProto.transport = TelephonyServiceState.Transport.TRANSPORT_WWAN;
            nriProto.rat = ServiceState.networkTypeToRilRadioTechnology(
                    nri.getAccessNetworkTechnology());
            nriList.add(nriProto);
            ssProto.networkRegistrationInfo =
                    new TelephonyServiceState.NetworkRegistrationInfo[nriList.size()];
            nriList.toArray(ssProto.networkRegistrationInfo);
        }

        ssProto.voiceRat = serviceState.getRilVoiceRadioTechnology();
        ssProto.dataRat = serviceState.getRilDataRadioTechnology();
        ssProto.channelNumber = serviceState.getChannelNumber();
        ssProto.nrFrequencyRange = serviceState.getNrFrequencyRange();
        ssProto.nrState = serviceState.getNrState();
        return ssProto;
    }

    /**
     * Annotate the call session with events
     *
     * @param timestamp Event timestamp
     * @param phoneId Phone id
     * @param eventBuilder Call session event builder
     */
    private synchronized void annotateInProgressCallSession(long timestamp, int phoneId,
                                                            CallSessionEventBuilder eventBuilder) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession != null) {
            callSession.addEvent(timestamp, eventBuilder);
        }
    }

    /**
     * Annotate the SMS session with events
     *
     * @param timestamp Event timestamp
     * @param phoneId Phone id
     * @param eventBuilder SMS session event builder
     */
    private synchronized void annotateInProgressSmsSession(long timestamp, int phoneId,
                                                           SmsSessionEventBuilder eventBuilder) {
        InProgressSmsSession smsSession = mInProgressSmsSessions.get(phoneId);
        if (smsSession != null) {
            smsSession.addEvent(timestamp, eventBuilder);
        }
    }

    /**
     * Create the call session if there isn't any existing one
     *
     * @param phoneId Phone id
     * @return The call session
     */
    private synchronized InProgressCallSession startNewCallSessionIfNeeded(int phoneId) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            logv("Starting a new call session on phone " + phoneId);
            callSession = new InProgressCallSession(phoneId);
            mInProgressCallSessions.append(phoneId, callSession);

            // Insert the latest service state, ims capabilities, and ims connection states as the
            // base.
            TelephonyServiceState serviceState = mLastServiceState.get(phoneId);
            if (serviceState != null) {
                callSession.addEvent(callSession.startElapsedTimeMs, new CallSessionEventBuilder(
                        TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
                        .setServiceState(serviceState));
            }

            ImsCapabilities imsCapabilities = mLastImsCapabilities.get(phoneId);
            if (imsCapabilities != null) {
                callSession.addEvent(callSession.startElapsedTimeMs, new CallSessionEventBuilder(
                        TelephonyCallSession.Event.Type.IMS_CAPABILITIES_CHANGED)
                        .setImsCapabilities(imsCapabilities));
            }

            ImsConnectionState imsConnectionState = mLastImsConnectionState.get(phoneId);
            if (imsConnectionState != null) {
                callSession.addEvent(callSession.startElapsedTimeMs, new CallSessionEventBuilder(
                        TelephonyCallSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
                        .setImsConnectionState(imsConnectionState));
            }
        }
        return callSession;
    }

    /**
     * Create the SMS session if there isn't any existing one
     *
     * @param phoneId Phone id
     * @return The SMS session
     */
    private synchronized InProgressSmsSession startNewSmsSessionIfNeeded(int phoneId) {
        InProgressSmsSession smsSession = mInProgressSmsSessions.get(phoneId);
        if (smsSession == null) {
            logv("Starting a new sms session on phone " + phoneId);
            smsSession = startNewSmsSession(phoneId);
            mInProgressSmsSessions.append(phoneId, smsSession);
        }
        return smsSession;
    }

    /**
     * Create a new SMS session
     *
     * @param phoneId Phone id
     * @return The SMS session
     */
    private InProgressSmsSession startNewSmsSession(int phoneId) {
        InProgressSmsSession smsSession = new InProgressSmsSession(phoneId);

        // Insert the latest service state, ims capabilities, and ims connection state as the
        // base.
        TelephonyServiceState serviceState = mLastServiceState.get(phoneId);
        if (serviceState != null) {
            smsSession.addEvent(smsSession.startElapsedTimeMs, new SmsSessionEventBuilder(
                    TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
                    .setServiceState(serviceState));
        }

        ImsCapabilities imsCapabilities = mLastImsCapabilities.get(phoneId);
        if (imsCapabilities != null) {
            smsSession.addEvent(smsSession.startElapsedTimeMs, new SmsSessionEventBuilder(
                    SmsSession.Event.Type.IMS_CAPABILITIES_CHANGED)
                    .setImsCapabilities(imsCapabilities));
        }

        ImsConnectionState imsConnectionState = mLastImsConnectionState.get(phoneId);
        if (imsConnectionState != null) {
            smsSession.addEvent(smsSession.startElapsedTimeMs, new SmsSessionEventBuilder(
                    SmsSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
                    .setImsConnectionState(imsConnectionState));
        }
        return smsSession;
    }

    /**
     * Finish the call session and move it into the completed session
     *
     * @param inProgressCallSession The in progress call session
     */
    private synchronized void finishCallSession(InProgressCallSession inProgressCallSession) {
        TelephonyCallSession callSession = new TelephonyCallSession();
        callSession.events = new TelephonyCallSession.Event[inProgressCallSession.events.size()];
        inProgressCallSession.events.toArray(callSession.events);
        callSession.startTimeMinutes = inProgressCallSession.startSystemTimeMin;
        callSession.phoneId = inProgressCallSession.phoneId;
        callSession.eventsDropped = inProgressCallSession.isEventsDropped();
        if (mCompletedCallSessions.size() >= MAX_COMPLETED_CALL_SESSIONS) {
            mCompletedCallSessions.removeFirst();
        }
        mCompletedCallSessions.add(callSession);
        mInProgressCallSessions.remove(inProgressCallSession.phoneId);
        logv("Call session finished");
    }

    /**
     * Finish the SMS session and move it into the completed session
     *
     * @param inProgressSmsSession The in progress SMS session
     */
    private synchronized void finishSmsSessionIfNeeded(InProgressSmsSession inProgressSmsSession) {
        if (inProgressSmsSession.getNumExpectedResponses() == 0) {
            SmsSession smsSession = finishSmsSession(inProgressSmsSession);

            mInProgressSmsSessions.remove(inProgressSmsSession.phoneId);
            logv("SMS session finished");
        }
    }

    private SmsSession finishSmsSession(InProgressSmsSession inProgressSmsSession) {
        SmsSession smsSession = new SmsSession();
        smsSession.events = new SmsSession.Event[inProgressSmsSession.events.size()];
        inProgressSmsSession.events.toArray(smsSession.events);
        smsSession.startTimeMinutes = inProgressSmsSession.startSystemTimeMin;
        smsSession.phoneId = inProgressSmsSession.phoneId;
        smsSession.eventsDropped = inProgressSmsSession.isEventsDropped();

        if (mCompletedSmsSessions.size() >= MAX_COMPLETED_SMS_SESSIONS) {
            mCompletedSmsSessions.removeFirst();
        }
        mCompletedSmsSessions.add(smsSession);
        return smsSession;
    }

    /**
     * Add telephony event into the queue
     *
     * @param event Telephony event
     */
    private synchronized void addTelephonyEvent(TelephonyEvent event) {
        if (mTelephonyEvents.size() >= MAX_TELEPHONY_EVENTS) {
            mTelephonyEvents.removeFirst();
            mTelephonyEventsDropped = true;
        }
        mTelephonyEvents.add(event);
    }

    /**
     * Write service changed event
     *
     * @param phoneId Phone id
     * @param serviceState Service state
     */
    public synchronized void writeServiceStateChanged(int phoneId, ServiceState serviceState) {

        TelephonyEvent event = new TelephonyEventBuilder(phoneId)
                .setServiceState(toServiceStateProto(serviceState)).build();

        // If service state doesn't change, we don't log the event.
        if (mLastServiceState.get(phoneId) != null &&
                Arrays.equals(TelephonyServiceState.toByteArray(mLastServiceState.get(phoneId)),
                        TelephonyServiceState.toByteArray(event.serviceState))) {
            return;
        }

        mLastServiceState.put(phoneId, event.serviceState);
        addTelephonyEvent(event);

        annotateInProgressCallSession(event.timestampMillis, phoneId,
                new CallSessionEventBuilder(
                        TelephonyCallSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
                        .setServiceState(event.serviceState));
        annotateInProgressSmsSession(event.timestampMillis, phoneId,
                new SmsSessionEventBuilder(
                        SmsSession.Event.Type.RIL_SERVICE_STATE_CHANGED)
                        .setServiceState(event.serviceState));
    }

    /**
     * Write data stall event
     *
     * @param phoneId Phone id
     * @param recoveryAction Data stall recovery action
     */
    public void writeDataStallEvent(int phoneId, int recoveryAction) {
        addTelephonyEvent(new TelephonyEventBuilder(phoneId)
                .setDataStallRecoveryAction(recoveryAction).build());
    }

    /**
     * Write SignalStrength event
     *
     * @param phoneId Phone id
     * @param signalStrength Signal strength at the time of data stall recovery
     */
    public void writeSignalStrengthEvent(int phoneId, int signalStrength) {
        addTelephonyEvent(new TelephonyEventBuilder(phoneId)
                .setSignalStrength(signalStrength).build());
    }

    /**
     * Write IMS feature settings changed event
     *
     * @param phoneId Phone id
     * @param feature IMS feature
     * @param network The IMS network type
     * @param value The settings. 0 indicates disabled, otherwise enabled.
     */
    public void writeImsSetFeatureValue(int phoneId, int feature, int network, int value) {
        TelephonySettings s = new TelephonySettings();
        if (network == ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
            switch (feature) {
                case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE:
                    s.isEnhanced4GLteModeEnabled = (value != 0);
                    break;
                case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO:
                    s.isVtOverLteEnabled = (value != 0);
                    break;
            }
        } else if (network == ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN) {
            switch (feature) {
                case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE:
                    s.isWifiCallingEnabled = (value != 0);
                    break;
                case MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO:
                    s.isVtOverWifiEnabled = (value != 0);
                    break;
            }
        }


        // If the settings don't change, we don't log the event.
        if (mLastSettings.get(phoneId) != null &&
                Arrays.equals(TelephonySettings.toByteArray(mLastSettings.get(phoneId)),
                        TelephonySettings.toByteArray(s))) {
            return;
        }

        mLastSettings.put(phoneId, s);

        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setSettings(s).build();
        addTelephonyEvent(event);

        annotateInProgressCallSession(event.timestampMillis, phoneId,
                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.SETTINGS_CHANGED)
                        .setSettings(s));
        annotateInProgressSmsSession(event.timestampMillis, phoneId,
                new SmsSessionEventBuilder(SmsSession.Event.Type.SETTINGS_CHANGED)
                        .setSettings(s));
    }

    /**
     * Write the preferred network settings changed event
     *
     * @param phoneId Phone id
     * @param networkType The preferred network
     */
    public void writeSetPreferredNetworkType(int phoneId, int networkType) {
        TelephonySettings s = new TelephonySettings();
        s.preferredNetworkMode = networkType + 1;

        // If the settings don't change, we don't log the event.
        if (mLastSettings.get(phoneId) != null &&
                Arrays.equals(TelephonySettings.toByteArray(mLastSettings.get(phoneId)),
                        TelephonySettings.toByteArray(s))) {
            return;
        }

        mLastSettings.put(phoneId, s);

        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setSettings(s).build());
    }

    /**
     * Write the IMS connection state changed event
     *
     * @param phoneId Phone id
     * @param state IMS connection state
     * @param reasonInfo The reason info. Only used for disconnected state.
     */
    public synchronized void writeOnImsConnectionState(int phoneId, int state,
                                                       ImsReasonInfo reasonInfo) {
        ImsConnectionState imsState = new ImsConnectionState();
        imsState.state = state;

        if (reasonInfo != null) {
            TelephonyProto.ImsReasonInfo ri = new TelephonyProto.ImsReasonInfo();

            ri.reasonCode = reasonInfo.getCode();
            ri.extraCode = reasonInfo.getExtraCode();
            String extraMessage = reasonInfo.getExtraMessage();
            if (extraMessage != null) {
                ri.extraMessage = extraMessage;
            }

            imsState.reasonInfo = ri;
        }

        // If the connection state does not change, do not log it.
        if (mLastImsConnectionState.get(phoneId) != null &&
                Arrays.equals(ImsConnectionState.toByteArray(mLastImsConnectionState.get(phoneId)),
                        ImsConnectionState.toByteArray(imsState))) {
            return;
        }

        mLastImsConnectionState.put(phoneId, imsState);

        TelephonyEvent event = new TelephonyEventBuilder(phoneId)
                .setImsConnectionState(imsState).build();
        addTelephonyEvent(event);

        annotateInProgressCallSession(event.timestampMillis, phoneId,
                new CallSessionEventBuilder(
                        TelephonyCallSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
                        .setImsConnectionState(event.imsConnectionState));
        annotateInProgressSmsSession(event.timestampMillis, phoneId,
                new SmsSessionEventBuilder(
                        SmsSession.Event.Type.IMS_CONNECTION_STATE_CHANGED)
                        .setImsConnectionState(event.imsConnectionState));
    }

    /**
     * Write the IMS capabilities changed event
     *
     * @param phoneId Phone id
     * @param capabilities IMS capabilities array
     */
    public synchronized void writeOnImsCapabilities(int phoneId,
            @ImsRegistrationImplBase.ImsRegistrationTech int radioTech,
            MmTelFeature.MmTelCapabilities capabilities) {
        ImsCapabilities cap = new ImsCapabilities();

        if (radioTech == ImsRegistrationImplBase.REGISTRATION_TECH_LTE) {
            cap.voiceOverLte = capabilities.isCapable(
                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
            cap.videoOverLte = capabilities.isCapable(
                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
            cap.utOverLte = capabilities.isCapable(
                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT);

        } else if (radioTech == ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN) {
            cap.voiceOverWifi = capabilities.isCapable(
                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VOICE);
            cap.videoOverWifi = capabilities.isCapable(
                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_VIDEO);
            cap.utOverWifi = capabilities.isCapable(
                    MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_UT);
        }

        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setImsCapabilities(cap).build();

        // If the capabilities don't change, we don't log the event.
        if (mLastImsCapabilities.get(phoneId) != null &&
                Arrays.equals(ImsCapabilities.toByteArray(mLastImsCapabilities.get(phoneId)),
                ImsCapabilities.toByteArray(cap))) {
            return;
        }

        mLastImsCapabilities.put(phoneId, cap);
        addTelephonyEvent(event);

        annotateInProgressCallSession(event.timestampMillis, phoneId,
                new CallSessionEventBuilder(
                        TelephonyCallSession.Event.Type.IMS_CAPABILITIES_CHANGED)
                        .setImsCapabilities(event.imsCapabilities));
        annotateInProgressSmsSession(event.timestampMillis, phoneId,
                new SmsSessionEventBuilder(
                        SmsSession.Event.Type.IMS_CAPABILITIES_CHANGED)
                        .setImsCapabilities(event.imsCapabilities));
    }

    /**
     * Convert PDP type into the enumeration
     *
     * @param type PDP type
     * @return The proto defined enumeration
     */
    private int toPdpType(String type) {
        switch (type) {
            case "IP":
                return PDP_TYPE_IP;
            case "IPV6":
                return PDP_TYPE_IPV6;
            case "IPV4V6":
                return PDP_TYPE_IPV4V6;
            case "PPP":
                return PDP_TYPE_PPP;
            case "NON-IP":
                return PDP_TYPE_NON_IP;
            case "UNSTRUCTURED":
                return PDP_TYPE_UNSTRUCTURED;
        }
        Rlog.e(TAG, "Unknown type: " + type);
        return PDP_UNKNOWN;
    }

    /**
     * Write setup data call event
     *
     * @param phoneId Phone id
     * @param radioTechnology The data call RAT
     * @param profileId Data profile id
     * @param apn APN in string
     * @param protocol Data connection protocol
     */
    public void writeSetupDataCall(int phoneId, int radioTechnology, int profileId, String apn,
                                   int protocol) {

        RilSetupDataCall setupDataCall = new RilSetupDataCall();
        setupDataCall.rat = radioTechnology;
        setupDataCall.dataProfile = profileId + 1;  // off by 1 between proto and RIL constants.
        if (apn != null) {
            setupDataCall.apn = apn;
        }

        setupDataCall.type = protocol + 1;

        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setSetupDataCall(
                setupDataCall).build());
    }

    /**
     * Write data call deactivate event
     *
     * @param phoneId Phone id
     * @param rilSerial RIL request serial number
     * @param cid call id
     * @param reason Deactivate reason
     */
    public void writeRilDeactivateDataCall(int phoneId, int rilSerial, int cid, int reason) {

        RilDeactivateDataCall deactivateDataCall = new RilDeactivateDataCall();
        deactivateDataCall.cid = cid;
        switch (reason) {
            case DataService.REQUEST_REASON_NORMAL:
                deactivateDataCall.reason = DeactivateReason.DEACTIVATE_REASON_NONE;
                break;
            case DataService.REQUEST_REASON_SHUTDOWN:
                deactivateDataCall.reason = DeactivateReason.DEACTIVATE_REASON_RADIO_OFF;
                break;
            case DataService.REQUEST_REASON_HANDOVER:
                deactivateDataCall.reason = DeactivateReason.DEACTIVATE_REASON_HANDOVER;
                break;
            default:
                deactivateDataCall.reason = DeactivateReason.DEACTIVATE_REASON_UNKNOWN;
        }

        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setDeactivateDataCall(
                deactivateDataCall).build());
    }

    /**
     * Write data call list event when connected
     * @param phoneId          Phone id
     * @param cid              Context Id, uniquely identifies the call
     * @param apnTypeBitmask   Bitmask of supported APN types
     * @param state            State of the data call event
     */
    public void writeRilDataCallEvent(int phoneId, int cid,
            int apnTypeBitmask, int state) {
        RilDataCall[] dataCalls = new RilDataCall[1];
        dataCalls[0] = new RilDataCall();
        dataCalls[0].cid = cid;
        dataCalls[0].apnTypeBitmask = apnTypeBitmask;
        dataCalls[0].state = state;

        SparseArray<RilDataCall> dataCallList;
        if (mLastRilDataCallEvents.get(phoneId) != null) {
            // If the Data call event does not change, do not log it.
            if (mLastRilDataCallEvents.get(phoneId).get(cid) != null
                    && Arrays.equals(
                        RilDataCall.toByteArray(mLastRilDataCallEvents.get(phoneId).get(cid)),
                        RilDataCall.toByteArray(dataCalls[0]))) {
                return;
            }
            dataCallList =  mLastRilDataCallEvents.get(phoneId);
        } else {
            dataCallList = new SparseArray<>();
        }

        dataCallList.put(cid, dataCalls[0]);
        mLastRilDataCallEvents.put(phoneId, dataCallList);
        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setDataCalls(dataCalls).build());
    }

    /**
     * Write CS call list event
     *
     * @param phoneId    Phone id
     * @param connections Array of GsmCdmaConnection objects
     */
    public void writeRilCallList(int phoneId, ArrayList<GsmCdmaConnection> connections,
                                 String countryIso) {
        logv("Logging CallList Changed Connections Size = " + connections.size());
        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "writeRilCallList: Call session is missing");
        } else {
            RilCall[] calls = convertConnectionsToRilCalls(connections, countryIso);
            callSession.addEvent(
                    new CallSessionEventBuilder(
                            TelephonyCallSession.Event.Type.RIL_CALL_LIST_CHANGED)
                            .setRilCalls(calls)
            );
            logv("Logged Call list changed");
            if (callSession.isPhoneIdle() && disconnectReasonsKnown(calls)) {
                finishCallSession(callSession);
            }
        }
    }

    private boolean disconnectReasonsKnown(RilCall[] calls) {
        for (RilCall call : calls) {
            if (call.callEndReason == 0) return false;
        }
        return true;
    }

    private RilCall[] convertConnectionsToRilCalls(ArrayList<GsmCdmaConnection> mConnections,
                                                   String countryIso) {
        RilCall[] calls = new RilCall[mConnections.size()];
        for (int i = 0; i < mConnections.size(); i++) {
            calls[i] = new RilCall();
            calls[i].index = i;
            convertConnectionToRilCall(mConnections.get(i), calls[i], countryIso);
        }
        return calls;
    }

    private EmergencyNumberInfo convertEmergencyNumberToEmergencyNumberInfo(EmergencyNumber num) {
        EmergencyNumberInfo emergencyNumberInfo = new EmergencyNumberInfo();
        emergencyNumberInfo.address = num.getNumber();
        emergencyNumberInfo.countryIso = num.getCountryIso();
        emergencyNumberInfo.mnc = num.getMnc();
        emergencyNumberInfo.serviceCategoriesBitmask = num.getEmergencyServiceCategoryBitmask();
        emergencyNumberInfo.urns = num.getEmergencyUrns().stream().toArray(String[]::new);
        emergencyNumberInfo.numberSourcesBitmask = num.getEmergencyNumberSourceBitmask();
        emergencyNumberInfo.routing = num.getEmergencyCallRouting();
        return emergencyNumberInfo;
    }

    private void convertConnectionToRilCall(GsmCdmaConnection conn, RilCall call,
                                            String countryIso) {
        if (conn.isIncoming()) {
            call.type = Type.MT;
        } else {
            call.type = Type.MO;
        }
        switch (conn.getState()) {
            case IDLE:
                call.state = CallState.CALL_IDLE;
                break;
            case ACTIVE:
                call.state = CallState.CALL_ACTIVE;
                break;
            case HOLDING:
                call.state = CallState.CALL_HOLDING;
                break;
            case DIALING:
                call.state = CallState.CALL_DIALING;
                break;
            case ALERTING:
                call.state = CallState.CALL_ALERTING;
                break;
            case INCOMING:
                call.state = CallState.CALL_INCOMING;
                break;
            case WAITING:
                call.state = CallState.CALL_WAITING;
                break;
            case DISCONNECTED:
                call.state = CallState.CALL_DISCONNECTED;
                break;
            case DISCONNECTING:
                call.state = CallState.CALL_DISCONNECTING;
                break;
            default:
                call.state = CallState.CALL_UNKNOWN;
                break;
        }
        call.callEndReason = conn.getDisconnectCause();
        call.isMultiparty = conn.isMultiparty();
        call.preciseDisconnectCause = conn.getPreciseDisconnectCause();

        // Emergency call metrics when call ends
        if (conn.getDisconnectCause() != DisconnectCause.NOT_DISCONNECTED
                && conn.isEmergencyCall() && conn.getEmergencyNumberInfo() != null) {
            /** Only collect this emergency number information per sample percentage */
            if (ThreadLocalRandom.current().nextDouble(0, 100)
                    < getSamplePercentageForEmergencyCall(countryIso)) {
                call.isEmergencyCall = conn.isEmergencyCall();
                call.emergencyNumberInfo = convertEmergencyNumberToEmergencyNumberInfo(
                        conn.getEmergencyNumberInfo());
                EmergencyNumberTracker emergencyNumberTracker = conn.getEmergencyNumberTracker();
                call.emergencyNumberDatabaseVersion = emergencyNumberTracker != null
                        ? emergencyNumberTracker.getEmergencyNumberDbVersion()
                        : TelephonyManager.INVALID_EMERGENCY_NUMBER_DB_VERSION;
            }
        }
    }

    /**
     * Write dial event
     *
     * @param phoneId Phone id
     * @param conn Connection object created to track this call
     * @param clirMode CLIR (Calling Line Identification Restriction) mode
     * @param uusInfo User-to-User signaling Info
     */
    public void writeRilDial(int phoneId, GsmCdmaConnection conn, int clirMode, UUSInfo uusInfo) {

        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);
        logv("Logging Dial Connection = " + conn);
        if (callSession == null) {
            Rlog.e(TAG, "writeRilDial: Call session is missing");
        } else {
            RilCall[] calls = new RilCall[1];
            calls[0] = new RilCall();
            calls[0].index = -1;
            convertConnectionToRilCall(conn, calls[0], "");
            callSession.addEvent(callSession.startElapsedTimeMs,
                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_REQUEST)
                            .setRilRequest(TelephonyCallSession.Event.RilRequest.RIL_REQUEST_DIAL)
                            .setRilCalls(calls));
            logv("Logged Dial event");
        }
    }

    /**
     * Write incoming call event
     *
     * @param phoneId Phone id
     * @param response Unused today
     */
    public void writeRilCallRing(int phoneId, char[] response) {
        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);

        callSession.addEvent(callSession.startElapsedTimeMs,
                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_CALL_RING));
    }

    /**
     * Write call hangup event
     *
     * @param phoneId Phone id
     * @param conn Connection object associated with the call that is being hung-up
     * @param callId Call id
     */
    public void writeRilHangup(int phoneId, GsmCdmaConnection conn, int callId,
                               String countryIso) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "writeRilHangup: Call session is missing");
        } else {
            RilCall[] calls = new RilCall[1];
            calls[0] = new RilCall();
            calls[0].index = callId;
            convertConnectionToRilCall(conn, calls[0], countryIso);
            callSession.addEvent(
                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_REQUEST)
                            .setRilRequest(TelephonyCallSession.Event.RilRequest.RIL_REQUEST_HANGUP)
                            .setRilCalls(calls));
            logv("Logged Hangup event");
        }
    }

    /**
     * Write call answer event
     *
     * @param phoneId Phone id
     * @param rilSerial RIL request serial number
     */
    public void writeRilAnswer(int phoneId, int rilSerial) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "writeRilAnswer: Call session is missing");
        } else {
            callSession.addEvent(
                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_REQUEST)
                            .setRilRequest(TelephonyCallSession.Event.RilRequest.RIL_REQUEST_ANSWER)
                            .setRilRequestId(rilSerial));
        }
    }

    /**
     * Write IMS call SRVCC event
     *
     * @param phoneId Phone id
     * @param rilSrvccState SRVCC state
     */
    public void writeRilSrvcc(int phoneId, int rilSrvccState) {
        InProgressCallSession callSession =  mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "writeRilSrvcc: Call session is missing");
        } else {
            callSession.addEvent(
                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.RIL_CALL_SRVCC)
                            .setSrvccState(rilSrvccState + 1));
        }
    }

    /**
     * Convert RIL request into proto defined RIL request
     *
     * @param r RIL request
     * @return RIL request defined in call session proto
     */
    private int toCallSessionRilRequest(int r) {
        switch (r) {
            case RILConstants.RIL_REQUEST_DIAL:
                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_DIAL;

            case RILConstants.RIL_REQUEST_ANSWER:
                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_ANSWER;

            case RILConstants.RIL_REQUEST_HANGUP:
            case RILConstants.RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND:
            case RILConstants.RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND:
                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_HANGUP;

            case RILConstants.RIL_REQUEST_SET_CALL_WAITING:
                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_SET_CALL_WAITING;

            case RILConstants.RIL_REQUEST_SWITCH_WAITING_OR_HOLDING_AND_ACTIVE:
                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_SWITCH_HOLDING_AND_ACTIVE;

            case RILConstants.RIL_REQUEST_CDMA_FLASH:
                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_CDMA_FLASH;

            case RILConstants.RIL_REQUEST_CONFERENCE:
                return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_CONFERENCE;
        }
        Rlog.e(TAG, "Unknown RIL request: " + r);
        return TelephonyCallSession.Event.RilRequest.RIL_REQUEST_UNKNOWN;
    }

    /**
     * Write setup data call response event
     *
     * @param phoneId Phone id
     * @param rilSerial RIL request serial number
     * @param rilError RIL error
     * @param rilRequest RIL request
     * @param result Data call result
     */
    private void writeOnSetupDataCallResponse(int phoneId, int rilSerial, int rilError,
                                              int rilRequest, DataCallResponse response) {

        RilSetupDataCallResponse setupDataCallResponse = new RilSetupDataCallResponse();
        RilDataCall dataCall = new RilDataCall();

        if (response != null) {
            setupDataCallResponse.status = (response.getCause() == 0
                    ? RilDataCallFailCause.PDP_FAIL_NONE : response.getCause());
            setupDataCallResponse.suggestedRetryTimeMillis = response.getSuggestedRetryTime();

            dataCall.cid = response.getId();
            dataCall.type = response.getProtocolType() + 1;

            if (!TextUtils.isEmpty(response.getInterfaceName())) {
                dataCall.iframe = response.getInterfaceName();
            }
        }
        setupDataCallResponse.call = dataCall;

        addTelephonyEvent(new TelephonyEventBuilder(phoneId)
                .setSetupDataCallResponse(setupDataCallResponse).build());
    }

    /**
     * Write call related solicited response event
     *
     * @param phoneId Phone id
     * @param rilSerial RIL request serial number
     * @param rilError RIL error
     * @param rilRequest RIL request
     */
    private void writeOnCallSolicitedResponse(int phoneId, int rilSerial, int rilError,
                                              int rilRequest) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "writeOnCallSolicitedResponse: Call session is missing");
        } else {
            callSession.addEvent(new CallSessionEventBuilder(
                    TelephonyCallSession.Event.Type.RIL_RESPONSE)
                    .setRilRequest(toCallSessionRilRequest(rilRequest))
                    .setRilRequestId(rilSerial)
                    .setRilError(rilError + 1));
        }
    }

    /**
     * Write SMS related solicited response event
     *
     * @param phoneId Phone id
     * @param rilSerial RIL request serial number
     * @param rilError RIL error
     * @param response SMS response
     */
    private synchronized void writeOnSmsSolicitedResponse(int phoneId, int rilSerial, int rilError,
                                                          SmsResponse response) {

        InProgressSmsSession smsSession = mInProgressSmsSessions.get(phoneId);
        if (smsSession == null) {
            Rlog.e(TAG, "SMS session is missing");
        } else {

            int errorCode = SmsResponse.NO_ERROR_CODE;
            if (response != null) {
                errorCode = response.mErrorCode;
            }

            smsSession.addEvent(new SmsSessionEventBuilder(
                    SmsSession.Event.Type.SMS_SEND_RESULT)
                    .setErrorCode(errorCode)
                    .setRilErrno(rilError + 1)
                    .setRilRequestId(rilSerial)
            );

            smsSession.decreaseExpectedResponse();
            finishSmsSessionIfNeeded(smsSession);
        }
    }

    /**
     * Write SMS related solicited response event
     *
     * @param phoneId Phone id
     * @param errorReason Defined in {@link SmsManager} RESULT_XXX.
     * @param messageId Unique id for this message.
     */
    public synchronized void writeOnImsServiceSmsSolicitedResponse(int phoneId,
            @ImsSmsImplBase.SendStatusResult int resultCode, int errorReason,
            long messageId) {

        InProgressSmsSession smsSession = mInProgressSmsSessions.get(phoneId);
        if (smsSession == null) {
            Rlog.e(TAG, "SMS session is missing");
        } else {

            smsSession.addEvent(new SmsSessionEventBuilder(
                    SmsSession.Event.Type.SMS_SEND_RESULT)
                    .setImsServiceErrno(resultCode)
                    .setErrorCode(errorReason)
                    .setMessageId((messageId))
            );

            smsSession.decreaseExpectedResponse();
            finishSmsSessionIfNeeded(smsSession);
        }
    }

    /**
     * Write deactivate data call response event
     *
     * @param phoneId Phone id
     * @param rilError RIL error
     */
    private void writeOnDeactivateDataCallResponse(int phoneId, int rilError) {
        addTelephonyEvent(new TelephonyEventBuilder(phoneId)
                .setDeactivateDataCallResponse(rilError + 1).build());
    }

    /**
     * Write RIL solicited response event
     *
     * @param phoneId Phone id
     * @param rilSerial RIL request serial number
     * @param rilError RIL error
     * @param rilRequest RIL request
     * @param ret The returned RIL response
     */
    public void writeOnRilSolicitedResponse(int phoneId, int rilSerial, int rilError,
                                            int rilRequest, Object ret) {
        switch (rilRequest) {
            case RIL_REQUEST_SETUP_DATA_CALL:
                DataCallResponse response = (DataCallResponse) ret;
                writeOnSetupDataCallResponse(phoneId, rilSerial, rilError, rilRequest, response);
                break;
            case RIL_REQUEST_DEACTIVATE_DATA_CALL:
                writeOnDeactivateDataCallResponse(phoneId, rilError);
                break;
            case RIL_REQUEST_HANGUP:
            case RIL_REQUEST_HANGUP_WAITING_OR_BACKGROUND:
            case RIL_REQUEST_HANGUP_FOREGROUND_RESUME_BACKGROUND:
            case RIL_REQUEST_DIAL:
            case RIL_REQUEST_ANSWER:
                writeOnCallSolicitedResponse(phoneId, rilSerial, rilError, rilRequest);
                break;
            case RIL_REQUEST_SEND_SMS:
            case RIL_REQUEST_SEND_SMS_EXPECT_MORE:
            case RIL_REQUEST_CDMA_SEND_SMS:
            case RIL_REQUEST_IMS_SEND_SMS:
                SmsResponse smsResponse = (SmsResponse) ret;
                writeOnSmsSolicitedResponse(phoneId, rilSerial, rilError, smsResponse);
                break;
        }
    }

    /**
     * Write network validation event.
     * @param networkValidationState the network validation state.
     */
    public void writeNetworkValidate(int networkValidationState) {
        addTelephonyEvent(
                new TelephonyEventBuilder().setNetworkValidate(networkValidationState).build());
    }

    /**
     * Write data switch event.
     * @param subId data switch to the subscription with this id.
     * @param dataSwitch the reason and state of data switch.
     */
    public void writeDataSwitch(int subId, DataSwitch dataSwitch) {
        int phoneId = SubscriptionManager.getPhoneId(subId);
        addTelephonyEvent(new TelephonyEventBuilder(phoneId).setDataSwitch(dataSwitch).build());
    }

    /**
     * Write on demand data switch event.
     * @param onDemandDataSwitch the apn and state of on demand data switch.
     */
    public void writeOnDemandDataSwitch(OnDemandDataSwitch onDemandDataSwitch) {
        addTelephonyEvent(
                new TelephonyEventBuilder().setOnDemandDataSwitch(onDemandDataSwitch).build());
    }

    /**
     * Write phone state changed event
     *
     * @param phoneId Phone id
     * @param phoneState Phone state. See PhoneConstants.State for the details.
     */
    public void writePhoneState(int phoneId, PhoneConstants.State phoneState) {
        int state;
        switch (phoneState) {
            case IDLE:
                state = TelephonyCallSession.Event.PhoneState.STATE_IDLE;
                break;
            case RINGING:
                state = TelephonyCallSession.Event.PhoneState.STATE_RINGING;
                break;
            case OFFHOOK:
                state = TelephonyCallSession.Event.PhoneState.STATE_OFFHOOK;
                break;
            default:
                state = TelephonyCallSession.Event.PhoneState.STATE_UNKNOWN;
                break;
        }

        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "writePhoneState: Call session is missing");
        } else {
            // For CS Calls Finish the Call Session after Receiving the Last Call Fail Cause
            // For IMS calls we receive the Disconnect Cause along with Call End event.
            // So we can finish the call session here.
            callSession.setLastKnownPhoneState(state);
            if ((state == TelephonyCallSession.Event.PhoneState.STATE_IDLE)
                    && (!callSession.containsCsCalls())) {
                finishCallSession(callSession);
            }
            callSession.addEvent(new CallSessionEventBuilder(
                    TelephonyCallSession.Event.Type.PHONE_STATE_CHANGED)
                    .setPhoneState(state));
        }
    }

    /**
     * Extracts the call ID from an ImsSession.
     *
     * @param session The session.
     * @return The call ID for the session, or -1 if none was found.
     */
    private int getCallId(ImsCallSession session) {
        if (session == null) {
            return -1;
        }

        try {
            return Integer.parseInt(session.getCallId());
        } catch (NumberFormatException nfe) {
            return -1;
        }
    }

    /**
     * Write IMS call state changed event
     *
     * @param phoneId Phone id
     * @param session IMS call session
     * @param callState IMS call state
     */
    public void writeImsCallState(int phoneId, ImsCallSession session,
                                  ImsPhoneCall.State callState) {
        int state;
        switch (callState) {
            case IDLE:
                state = TelephonyCallSession.Event.CallState.CALL_IDLE; break;
            case ACTIVE:
                state = TelephonyCallSession.Event.CallState.CALL_ACTIVE; break;
            case HOLDING:
                state = TelephonyCallSession.Event.CallState.CALL_HOLDING; break;
            case DIALING:
                state = TelephonyCallSession.Event.CallState.CALL_DIALING; break;
            case ALERTING:
                state = TelephonyCallSession.Event.CallState.CALL_ALERTING; break;
            case INCOMING:
                state = TelephonyCallSession.Event.CallState.CALL_INCOMING; break;
            case WAITING:
                state = TelephonyCallSession.Event.CallState.CALL_WAITING; break;
            case DISCONNECTED:
                state = TelephonyCallSession.Event.CallState.CALL_DISCONNECTED; break;
            case DISCONNECTING:
                state = TelephonyCallSession.Event.CallState.CALL_DISCONNECTING; break;
            default:
                state = TelephonyCallSession.Event.CallState.CALL_UNKNOWN; break;
        }

        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "Call session is missing");
        } else {
            callSession.addEvent(new CallSessionEventBuilder(
                    TelephonyCallSession.Event.Type.IMS_CALL_STATE_CHANGED)
                    .setCallIndex(getCallId(session))
                    .setCallState(state));
        }
    }

    /**
     * Write IMS call start event
     *
     * @param phoneId Phone id
     * @param session IMS call session
     */
    public void writeOnImsCallStart(int phoneId, ImsCallSession session) {
        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);

        callSession.addEvent(
                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_COMMAND)
                        .setCallIndex(getCallId(session))
                        .setImsCommand(TelephonyCallSession.Event.ImsCommand.IMS_CMD_START));
    }

    /**
     * Write IMS incoming call event
     *
     * @param phoneId Phone id
     * @param session IMS call session
     */
    public void writeOnImsCallReceive(int phoneId, ImsCallSession session) {
        InProgressCallSession callSession = startNewCallSessionIfNeeded(phoneId);

        callSession.addEvent(
                new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_CALL_RECEIVE)
                        .setCallIndex(getCallId(session)));
    }

    /**
     * Write IMS command event
     *
     * @param phoneId Phone id
     * @param session IMS call session
     * @param command IMS command
     */
    public void writeOnImsCommand(int phoneId, ImsCallSession session, int command) {

        InProgressCallSession callSession =  mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "Call session is missing");
        } else {
            callSession.addEvent(
                    new CallSessionEventBuilder(TelephonyCallSession.Event.Type.IMS_COMMAND)
                            .setCallIndex(getCallId(session))
                            .setImsCommand(command));
        }
    }

    /**
     * Convert IMS reason info into proto
     *
     * @param reasonInfo IMS reason info
     * @return Converted proto
     */
    private TelephonyProto.ImsReasonInfo toImsReasonInfoProto(ImsReasonInfo reasonInfo) {
        TelephonyProto.ImsReasonInfo ri = new TelephonyProto.ImsReasonInfo();
        if (reasonInfo != null) {
            ri.reasonCode = reasonInfo.getCode();
            ri.extraCode = reasonInfo.getExtraCode();
            String extraMessage = reasonInfo.getExtraMessage();
            if (extraMessage != null) {
                ri.extraMessage = extraMessage;
            }
        }
        return ri;
    }

    /**
     * Convert CallQuality to proto.
     *
     * @param callQuality call quality to convert
     * @return Coverted proto
     */
    public static TelephonyCallSession.Event.CallQuality toCallQualityProto(
            CallQuality callQuality) {
        TelephonyCallSession.Event.CallQuality cq = new TelephonyCallSession.Event.CallQuality();
        if (callQuality != null) {
            cq.downlinkLevel = callQualityLevelToProtoEnum(callQuality
                    .getDownlinkCallQualityLevel());
            cq.uplinkLevel = callQualityLevelToProtoEnum(callQuality.getUplinkCallQualityLevel());
            // callDuration is reported in millis, so convert to seconds
            cq.durationInSeconds = callQuality.getCallDuration() / 1000;
            cq.rtpPacketsTransmitted = callQuality.getNumRtpPacketsTransmitted();
            cq.rtpPacketsReceived = callQuality.getNumRtpPacketsReceived();
            cq.rtpPacketsTransmittedLost = callQuality.getNumRtpPacketsTransmittedLost();
            cq.rtpPacketsNotReceived = callQuality.getNumRtpPacketsNotReceived();
            cq.averageRelativeJitterMillis = callQuality.getAverageRelativeJitter();
            cq.maxRelativeJitterMillis = callQuality.getMaxRelativeJitter();
            cq.codecType = convertImsCodec(callQuality.getCodecType());
            cq.rtpInactivityDetected = callQuality.isRtpInactivityDetected();
            cq.rxSilenceDetected = callQuality.isIncomingSilenceDetectedAtCallSetup();
            cq.txSilenceDetected = callQuality.isOutgoingSilenceDetectedAtCallSetup();
        }
        return cq;
    }

    /**
     * Convert Call quality level into proto defined value.
     */
    private static int callQualityLevelToProtoEnum(int level) {
        if (level == CallQuality.CALL_QUALITY_EXCELLENT) {
            return TelephonyCallSession.Event.CallQuality.CallQualityLevel.EXCELLENT;
        } else if (level == CallQuality.CALL_QUALITY_GOOD) {
            return TelephonyCallSession.Event.CallQuality.CallQualityLevel.GOOD;
        } else if (level == CallQuality.CALL_QUALITY_FAIR) {
            return TelephonyCallSession.Event.CallQuality.CallQualityLevel.FAIR;
        } else if (level == CallQuality.CALL_QUALITY_POOR) {
            return TelephonyCallSession.Event.CallQuality.CallQualityLevel.POOR;
        } else if (level == CallQuality.CALL_QUALITY_BAD) {
            return TelephonyCallSession.Event.CallQuality.CallQualityLevel.BAD;
        } else if (level == CallQuality.CALL_QUALITY_NOT_AVAILABLE) {
            return TelephonyCallSession.Event.CallQuality.CallQualityLevel.NOT_AVAILABLE;
        } else {
            return TelephonyCallSession.Event.CallQuality.CallQualityLevel.UNDEFINED;
        }
    }

    /**
     * Write IMS call end event
     *
     * @param phoneId Phone id
     * @param session IMS call session
     * @param reasonInfo Call end reason
     * @param cqm Call Quality Metrics
     * @param emergencyNumber Emergency Number Info
     * @param countryIso Network country iso
     * @param emergencyNumberDatabaseVersion Emergency Number Database Version
     */
    public void writeOnImsCallTerminated(int phoneId, ImsCallSession session,
                                         ImsReasonInfo reasonInfo, CallQualityMetrics cqm,
                                         EmergencyNumber emergencyNumber, String countryIso,
                                         int emergencyNumberDatabaseVersion) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "Call session is missing");
        } else {
            CallSessionEventBuilder callSessionEvent = new CallSessionEventBuilder(
                    TelephonyCallSession.Event.Type.IMS_CALL_TERMINATED);
            callSessionEvent.setCallIndex(getCallId(session));
            callSessionEvent.setImsReasonInfo(toImsReasonInfoProto(reasonInfo));

            if (cqm != null) {
                callSessionEvent.setCallQualitySummaryDl(cqm.getCallQualitySummaryDl())
                        .setCallQualitySummaryUl(cqm.getCallQualitySummaryUl());
            }

            if (emergencyNumber != null) {
                /** Only collect this emergency number information per sample percentage */
                if (ThreadLocalRandom.current().nextDouble(0, 100)
                        < getSamplePercentageForEmergencyCall(countryIso)) {
                    callSessionEvent.setIsImsEmergencyCall(true);
                    callSessionEvent.setImsEmergencyNumberInfo(
                            convertEmergencyNumberToEmergencyNumberInfo(emergencyNumber));
                    callSessionEvent.setEmergencyNumberDatabaseVersion(
                            emergencyNumberDatabaseVersion);
                }
            }
            callSession.addEvent(callSessionEvent);
        }
    }

    /**
     * Write IMS call hangover event
     *
     * @param phoneId Phone id
     * @param eventType hangover type
     * @param session IMS call session
     * @param srcAccessTech Hangover starting RAT
     * @param targetAccessTech Hangover destination RAT
     * @param reasonInfo Hangover reason
     */
    public void writeOnImsCallHandoverEvent(int phoneId, int eventType, ImsCallSession session,
                                            int srcAccessTech, int targetAccessTech,
                                            ImsReasonInfo reasonInfo) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "Call session is missing");
        } else {
            callSession.addEvent(
                    new CallSessionEventBuilder(eventType)
                            .setCallIndex(getCallId(session))
                            .setSrcAccessTech(srcAccessTech)
                            .setTargetAccessTech(targetAccessTech)
                            .setImsReasonInfo(toImsReasonInfoProto(reasonInfo)));
        }
    }

    /**
     * Write Send SMS event
     *
     * @param phoneId Phone id
     * @param rilSerial RIL request serial number
     * @param tech SMS RAT
     * @param format SMS format. Either 3GPP or 3GPP2.
     */
    public synchronized void writeRilSendSms(int phoneId, int rilSerial, int tech, int format) {
        InProgressSmsSession smsSession = startNewSmsSessionIfNeeded(phoneId);

        smsSession.addEvent(new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_SEND)
                .setTech(tech)
                .setRilRequestId(rilSerial)
                .setFormat(format)
        );

        smsSession.increaseExpectedResponse();
    }

    /**
     * Write Send SMS event using ImsService. Expecting response from
     * {@link #writeOnSmsSolicitedResponse}.
     *
     * @param phoneId Phone id
     * @param format SMS format. Either {@link SmsMessage#FORMAT_3GPP} or
     *         {@link SmsMessage#FORMAT_3GPP2}.
     * @param resultCode The result of sending the new SMS to the vendor layer to be sent to the
     *         carrier network.
     */
    public synchronized void writeImsServiceSendSms(int phoneId, String format,
            @ImsSmsImplBase.SendStatusResult int resultCode) {
        InProgressSmsSession smsSession = startNewSmsSessionIfNeeded(phoneId);
        smsSession.addEvent(new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_SEND)
                .setTech(SmsSession.Event.Tech.SMS_IMS)
                .setImsServiceErrno(resultCode)
                .setFormat(convertSmsFormat(format))
        );

        smsSession.increaseExpectedResponse();
    }

    /**
     * Write incoming Broadcast SMS event
     *
     * @param phoneId Phone id
     * @param format CB msg format
     * @param priority CB msg priority
     * @param isCMAS true if msg is CMAS
     * @param isETWS true if msg is ETWS
     * @param serviceCategory Service category of CB msg
     * @param serialNumber Serial number of the message
     * @param deliveredTimestamp Message's delivered timestamp
     */
    public synchronized void writeNewCBSms(int phoneId, int format, int priority, boolean isCMAS,
                                           boolean isETWS, int serviceCategory, int serialNumber,
                                           long deliveredTimestamp) {
        InProgressSmsSession smsSession = startNewSmsSessionIfNeeded(phoneId);

        int type;
        if (isCMAS) {
            type = SmsSession.Event.CBMessageType.CMAS;
        } else if (isETWS) {
            type = SmsSession.Event.CBMessageType.ETWS;
        } else {
            type = SmsSession.Event.CBMessageType.OTHER;
        }

        SmsSession.Event.CBMessage cbm = new SmsSession.Event.CBMessage();
        cbm.msgFormat = format;
        cbm.msgPriority = priority + 1;
        cbm.msgType = type;
        cbm.serviceCategory = serviceCategory;
        cbm.serialNumber = serialNumber;
        cbm.deliveredTimestampMillis = deliveredTimestamp;

        smsSession.addEvent(new SmsSessionEventBuilder(SmsSession.Event.Type.CB_SMS_RECEIVED)
                .setCellBroadcastMessage(cbm)
        );

        finishSmsSessionIfNeeded(smsSession);
    }

    /**
     * Write an incoming multi-part SMS that was discarded because some parts were missing
     *
     * @param phoneId Phone id
     * @param format SMS format. Either 3GPP or 3GPP2.
     * @param receivedCount Number of received parts.
     * @param totalCount Total number of parts of the SMS.
     */
    public void writeDroppedIncomingMultipartSms(int phoneId, String format,
            int receivedCount, int totalCount) {
        logv("Logged dropped multipart SMS: received " + receivedCount
                + " out of " + totalCount);

        SmsSession.Event.IncompleteSms details = new SmsSession.Event.IncompleteSms();
        details.receivedParts = receivedCount;
        details.totalParts = totalCount;

        InProgressSmsSession smsSession = startNewSmsSession(phoneId);
        smsSession.addEvent(
                new SmsSessionEventBuilder(SmsSession.Event.Type.INCOMPLETE_SMS_RECEIVED)
                    .setFormat(convertSmsFormat(format))
                    .setIncompleteSms(details));

        finishSmsSession(smsSession);
    }

    /**
     * Write a generic SMS of any type
     *
     * @param phoneId Phone id
     * @param type Type of the SMS.
     * @param format SMS format. Either 3GPP or 3GPP2.
     * @param success Indicates if the SMS-PP was successfully delivered to the USIM.
     */
    private void writeIncomingSmsWithType(int phoneId, int type, String format, boolean success) {
        InProgressSmsSession smsSession = startNewSmsSession(phoneId);
        smsSession.addEvent(new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_RECEIVED)
                .setFormat(convertSmsFormat(format))
                .setSmsType(type)
                .setErrorCode(success ? SmsManager.RESULT_ERROR_NONE :
                    SmsManager.RESULT_ERROR_GENERIC_FAILURE));
        finishSmsSession(smsSession);
    }

    /**
     * Write an incoming SMS-PP for the USIM
     *
     * @param phoneId Phone id
     * @param format SMS format. Either 3GPP or 3GPP2.
     * @param success Indicates if the SMS-PP was successfully delivered to the USIM.
     */
    public void writeIncomingSMSPP(int phoneId, String format, boolean success) {
        logv("Logged SMS-PP session. Result = " + success);
        writeIncomingSmsWithType(phoneId,
                SmsSession.Event.SmsType.SMS_TYPE_SMS_PP, format, success);
    }

    /**
     * Write an incoming SMS to update voicemail indicator
     *
     * @param phoneId Phone id
     * @param format SMS format. Either 3GPP or 3GPP2.
     */
    public void writeIncomingVoiceMailSms(int phoneId, String format) {
        logv("Logged VoiceMail message.");
        writeIncomingSmsWithType(phoneId,
                SmsSession.Event.SmsType.SMS_TYPE_VOICEMAIL_INDICATION, format, true);
    }

    /**
     * Write an incoming SMS of type 0
     *
     * @param phoneId Phone id
     * @param format SMS format. Either 3GPP or 3GPP2.
     */
    public void writeIncomingSmsTypeZero(int phoneId, String format) {
        logv("Logged Type-0 SMS message.");
        writeIncomingSmsWithType(phoneId,
                SmsSession.Event.SmsType.SMS_TYPE_ZERO, format, true);
    }

    /**
     * Write a successful incoming SMS session
     *
     * @param phoneId Phone id
     * @param type Type of the SMS.
     * @param smsOverIms true if the SMS was received over SMS, false otherwise
     * @param format SMS format. Either 3GPP or 3GPP2.
     * @param timestamps array with timestamps of each incoming SMS part. It contains a single
     * @param blocked indicates if the message was blocked or not.
     * @param success Indicates if the SMS-PP was successfully delivered to the USIM.
     * @param messageId Unique id for this message.
     */
    private void writeIncomingSmsSessionWithType(int phoneId, int type, boolean smsOverIms,
            String format, long[] timestamps, boolean blocked, boolean success,
            long messageId) {
        logv("Logged SMS session consisting of " + timestamps.length
                + " parts, over IMS = " + smsOverIms
                + " blocked = " + blocked
                + " type = " + type
                + " messageId = " + messageId);

        InProgressSmsSession smsSession = startNewSmsSession(phoneId);
        for (long time : timestamps) {
            SmsSessionEventBuilder eventBuilder =
                    new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_RECEIVED)
                        .setFormat(convertSmsFormat(format))
                        .setTech(smsOverIms ? SmsSession.Event.Tech.SMS_IMS :
                            SmsSession.Event.Tech.SMS_GSM)
                        .setErrorCode(success ? SmsManager.RESULT_ERROR_NONE :
                            SmsManager.RESULT_ERROR_GENERIC_FAILURE)
                        .setSmsType(type)
                        .setBlocked(blocked)
                        .setMessageId(messageId);
            smsSession.addEvent(time, eventBuilder);
        }
        finishSmsSession(smsSession);
    }

    /**
     * Write an incoming WAP-PUSH message.
     *
     * @param phoneId Phone id
     * @param smsOverIms true if the SMS was received over SMS, false otherwise
     * @param format SMS format. Either 3GPP or 3GPP2.
     * @param timestamps array with timestamps of each incoming SMS part. It contains a single
     * @param success Indicates if the SMS-PP was successfully delivered to the USIM.
     * @param messageId Unique id for this message.
     */
    public void writeIncomingWapPush(int phoneId, boolean smsOverIms, String format,
            long[] timestamps, boolean success, long messageId) {
        writeIncomingSmsSessionWithType(phoneId, SmsSession.Event.SmsType.SMS_TYPE_WAP_PUSH,
                smsOverIms, format, timestamps, false, success, messageId);
    }

    /**
     * Write a successful incoming SMS session
     *
     * @param phoneId Phone id
     * @param smsOverIms true if the SMS was received over SMS, false otherwise
     * @param format SMS format. Either 3GPP or 3GPP2.
     * @param timestamps array with timestamps of each incoming SMS part. It contains a single
     * @param blocked indicates if the message was blocked or not.
     * @param messageId Unique id for this message.
     */
    public void writeIncomingSmsSession(int phoneId, boolean smsOverIms, String format,
            long[] timestamps, boolean blocked, long messageId) {
        writeIncomingSmsSessionWithType(phoneId, SmsSession.Event.SmsType.SMS_TYPE_NORMAL,
                smsOverIms, format, timestamps, blocked, true, messageId);
    }

    /**
     * Write an error incoming SMS
     *
     * @param phoneId Phone id
     * @param smsOverIms true if the SMS was received over SMS, false otherwise
     * @param result Indicates the reason of the failure.
     */
    public void writeIncomingSmsError(int phoneId, boolean smsOverIms, int result) {
        logv("Incoming SMS error = " + result);

        int smsError = SmsManager.RESULT_ERROR_GENERIC_FAILURE;
        switch (result) {
            case Intents.RESULT_SMS_HANDLED:
                // This should not happen.
                return;
            case Intents.RESULT_SMS_OUT_OF_MEMORY:
                smsError = SmsManager.RESULT_NO_MEMORY;
                break;
            case Intents.RESULT_SMS_UNSUPPORTED:
                smsError = SmsManager.RESULT_REQUEST_NOT_SUPPORTED;
                break;
            case Intents.RESULT_SMS_GENERIC_ERROR:
            default:
                smsError = SmsManager.RESULT_ERROR_GENERIC_FAILURE;
                break;
        }

        InProgressSmsSession smsSession = startNewSmsSession(phoneId);

        SmsSessionEventBuilder eventBuilder =
                new SmsSessionEventBuilder(SmsSession.Event.Type.SMS_RECEIVED)
                    .setErrorCode(smsError)
                    .setTech(smsOverIms ? SmsSession.Event.Tech.SMS_IMS :
                        SmsSession.Event.Tech.SMS_GSM);
        smsSession.addEvent(eventBuilder);
        finishSmsSession(smsSession);
    }

    /**
     * Write NITZ event
     *
     * @param phoneId Phone id
     * @param timestamp NITZ time in milliseconds
     */
    public void writeNITZEvent(int phoneId, long timestamp) {
        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setNITZ(timestamp).build();
        addTelephonyEvent(event);

        annotateInProgressCallSession(event.timestampMillis, phoneId,
                new CallSessionEventBuilder(
                        TelephonyCallSession.Event.Type.NITZ_TIME)
                        .setNITZ(timestamp));
    }

    /**
     * Write Modem Restart event
     *
     * @param phoneId Phone id
     * @param reason Reason for the modem reset.
     */
    public void writeModemRestartEvent(int phoneId, String reason) {
        final ModemRestart modemRestart = new ModemRestart();
        String basebandVersion = Build.getRadioVersion();
        if (basebandVersion != null) modemRestart.basebandVersion = basebandVersion;
        if (reason != null) modemRestart.reason = reason;
        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setModemRestart(
                modemRestart).build();
        addTelephonyEvent(event);
    }

    /**
     * Write carrier identification matching event
     *
     * @param phoneId Phone id
     * @param version Carrier table version
     * @param cid Unique Carrier Id
     * @param unknownMcmnc MCC and MNC that map to this carrier
     * @param unknownGid1 Group id level 1
     * @param simInfo Subscription info
     */
    public void writeCarrierIdMatchingEvent(int phoneId, int version, int cid,
                                            String unknownMcmnc, String unknownGid1,
                                            CarrierResolver.CarrierMatchingRule simInfo) {
        final CarrierIdMatching carrierIdMatching = new CarrierIdMatching();
        final CarrierIdMatchingResult carrierIdMatchingResult = new CarrierIdMatchingResult();

        // fill in information for unknown mccmnc and gid1 for unidentified carriers.
        if (cid != TelephonyManager.UNKNOWN_CARRIER_ID) {
            // Successful matching event if result only has carrierId
            carrierIdMatchingResult.carrierId = cid;
            // Unknown Gid1 event if result only has carrierId, gid1 and mccmnc
            if (unknownGid1 != null) {
                carrierIdMatchingResult.unknownMccmnc = unknownMcmnc;
                carrierIdMatchingResult.unknownGid1 = unknownGid1;
            }
        } else {
            // Unknown mccmnc event if result only has mccmnc
            if (unknownMcmnc != null) {
                carrierIdMatchingResult.unknownMccmnc = unknownMcmnc;
            }
        }

        // fill in complete matching information from the SIM.
        carrierIdMatchingResult.mccmnc = TelephonyUtils.emptyIfNull(simInfo.mccMnc);
        carrierIdMatchingResult.spn = TelephonyUtils.emptyIfNull(simInfo.spn);
        carrierIdMatchingResult.pnn = TelephonyUtils.emptyIfNull(simInfo.plmn);
        carrierIdMatchingResult.gid1 = TelephonyUtils.emptyIfNull(simInfo.gid1);
        carrierIdMatchingResult.gid2 = TelephonyUtils.emptyIfNull(simInfo.gid2);
        carrierIdMatchingResult.imsiPrefix = TelephonyUtils.emptyIfNull(simInfo.imsiPrefixPattern);
        carrierIdMatchingResult.iccidPrefix = TelephonyUtils.emptyIfNull(simInfo.iccidPrefix);
        carrierIdMatchingResult.preferApn = TelephonyUtils.emptyIfNull(simInfo.apn);
        if (simInfo.privilegeAccessRule != null) {
            carrierIdMatchingResult.privilegeAccessRule =
                    simInfo.privilegeAccessRule.stream().toArray(String[]::new);
        }

        carrierIdMatching.cidTableVersion = version;
        carrierIdMatching.result = carrierIdMatchingResult;

        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setCarrierIdMatching(
                carrierIdMatching).build();
        mLastCarrierId.put(phoneId, carrierIdMatching);
        addTelephonyEvent(event);
    }

    /**
     * Write emergency number update event
     *
     * @param emergencyNumber Updated emergency number
     */
    public void writeEmergencyNumberUpdateEvent(int phoneId, EmergencyNumber emergencyNumber,
            int emergencyNumberDatabaseVersion) {
        if (emergencyNumber == null) {
            return;
        }
        final EmergencyNumberInfo emergencyNumberInfo =
                convertEmergencyNumberToEmergencyNumberInfo(emergencyNumber);

        TelephonyEvent event = new TelephonyEventBuilder(phoneId).setUpdatedEmergencyNumber(
                emergencyNumberInfo, emergencyNumberDatabaseVersion).build();
        addTelephonyEvent(event);
    }

    /**
     * Write network capabilities changed event
     *
     * @param phoneId Phone id
     * @param networkCapabilities Network capabilities
     */
    public void writeNetworkCapabilitiesChangedEvent(int phoneId,
            NetworkCapabilities networkCapabilities) {
        final NetworkCapabilitiesInfo caps = new NetworkCapabilitiesInfo();
        caps.isNetworkUnmetered = networkCapabilities.hasCapability(
                NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED);

        TelephonyEvent event = new TelephonyEventBuilder(phoneId)
                .setNetworkCapabilities(caps).build();
        mLastNetworkCapabilitiesInfos.put(phoneId, caps);
        addTelephonyEvent(event);
    }

    /**
     * Convert SMS format
     */
    private int convertSmsFormat(String format) {
        int formatCode = SmsSession.Event.Format.SMS_FORMAT_UNKNOWN;
        switch (format) {
            case SmsMessage.FORMAT_3GPP : {
                formatCode = SmsSession.Event.Format.SMS_FORMAT_3GPP;
                break;
            }
            case SmsMessage.FORMAT_3GPP2: {
                formatCode = SmsSession.Event.Format.SMS_FORMAT_3GPP2;
                break;
            }
        }
        return formatCode;
    }

    /**
     * Convert IMS audio codec into proto defined value
     *
     * @param c IMS codec value
     * @return Codec value defined in call session proto
     */
    private static int convertImsCodec(int c) {
        switch (c) {
            case ImsStreamMediaProfile.AUDIO_QUALITY_AMR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_AMR;
            case ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_AMR_WB;
            case ImsStreamMediaProfile.AUDIO_QUALITY_QCELP13K:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_QCELP13K;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVRC:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_B:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC_B;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC_WB;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_NW:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC_NW;
            case ImsStreamMediaProfile.AUDIO_QUALITY_GSM_EFR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_GSM_EFR;
            case ImsStreamMediaProfile.AUDIO_QUALITY_GSM_FR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_GSM_FR;
            case ImsStreamMediaProfile.AUDIO_QUALITY_GSM_HR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_GSM_HR;
            case ImsStreamMediaProfile.AUDIO_QUALITY_G711U:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_G711U;
            case ImsStreamMediaProfile.AUDIO_QUALITY_G723:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_G723;
            case ImsStreamMediaProfile.AUDIO_QUALITY_G711A:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_G711A;
            case ImsStreamMediaProfile.AUDIO_QUALITY_G722:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_G722;
            case ImsStreamMediaProfile.AUDIO_QUALITY_G711AB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_G711AB;
            case ImsStreamMediaProfile.AUDIO_QUALITY_G729:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_G729;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVS_NB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVS_NB;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVS_WB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVS_WB;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVS_SWB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVS_SWB;
            case ImsStreamMediaProfile.AUDIO_QUALITY_EVS_FB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVS_FB;
            default:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_UNKNOWN;
        }
    }

    /**
     * Convert GSM/CDMA audio codec into proto defined value
     *
     * @param c GSM/CDMA codec value
     * @return Codec value defined in call session proto
     */
    private int convertGsmCdmaCodec(int c) {
        switch (c) {
            case DriverCall.AUDIO_QUALITY_AMR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_AMR;
            case DriverCall.AUDIO_QUALITY_AMR_WB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_AMR_WB;
            case DriverCall.AUDIO_QUALITY_GSM_EFR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_GSM_EFR;
            case DriverCall.AUDIO_QUALITY_GSM_FR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_GSM_FR;
            case DriverCall.AUDIO_QUALITY_GSM_HR:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_GSM_HR;
            case DriverCall.AUDIO_QUALITY_EVRC:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC;
            case DriverCall.AUDIO_QUALITY_EVRC_B:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC_B;
            case DriverCall.AUDIO_QUALITY_EVRC_WB:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC_WB;
            case DriverCall.AUDIO_QUALITY_EVRC_NW:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_EVRC_NW;
            default:
                return TelephonyCallSession.Event.AudioCodec.AUDIO_CODEC_UNKNOWN;
        }
    }

    /**
     * Write audio codec event
     *
     * @param phoneId Phone id
     * @param session IMS call session
     */
    public void writeAudioCodecIms(int phoneId, ImsCallSession session) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "Call session is missing");
            return;
        }

        ImsCallProfile localCallProfile = session.getLocalCallProfile();
        if (localCallProfile != null) {
            int codec = convertImsCodec(localCallProfile.mMediaProfile.mAudioQuality);
            callSession.addEvent(new CallSessionEventBuilder(
                    TelephonyCallSession.Event.Type.AUDIO_CODEC)
                    .setCallIndex(getCallId(session))
                    .setAudioCodec(codec));

            logv("Logged Audio Codec event. Value: " + codec);
        }
    }

    /**
     * Write audio codec event
     *
     * @param phoneId Phone id
     * @param audioQuality Audio quality value
     */
    public void writeAudioCodecGsmCdma(int phoneId, int audioQuality) {
        InProgressCallSession callSession = mInProgressCallSessions.get(phoneId);
        if (callSession == null) {
            Rlog.e(TAG, "Call session is missing");
            return;
        }

        int codec = convertGsmCdmaCodec(audioQuality);
        callSession.addEvent(new CallSessionEventBuilder(
                TelephonyCallSession.Event.Type.AUDIO_CODEC)
                .setAudioCodec(codec));

        logv("Logged Audio Codec event. Value: " + codec);
    }

    //TODO: Expand the proto in the future
    public void writeOnImsCallProgressing(int phoneId, ImsCallSession session) {}
    public void writeOnImsCallStarted(int phoneId, ImsCallSession session) {}
    public void writeOnImsCallStartFailed(int phoneId, ImsCallSession session,
                                          ImsReasonInfo reasonInfo) {}
    public void writeOnImsCallHeld(int phoneId, ImsCallSession session) {}
    public void writeOnImsCallHoldReceived(int phoneId, ImsCallSession session) {}
    public void writeOnImsCallHoldFailed(int phoneId, ImsCallSession session,
                                         ImsReasonInfo reasonInfo) {}
    public void writeOnImsCallResumed(int phoneId, ImsCallSession session) {}
    public void writeOnImsCallResumeReceived(int phoneId, ImsCallSession session) {}
    public void writeOnImsCallResumeFailed(int phoneId, ImsCallSession session,
                                           ImsReasonInfo reasonInfo) {}
    public void writeOnRilTimeoutResponse(int phoneId, int rilSerial, int rilRequest) {}

    /**
     * Get the sample percentage of collecting metrics based on countries' population.
     *
     * The larger population the country has, the lower percentage we use to collect this
     * metrics. Since the exact population changes frequently, buckets of the population are used
     * instead of its exact number. Seven different levels of sampling percentage are assigned
     * based on the scale of population for countries.
     */
    private double getSamplePercentageForEmergencyCall(String countryIso) {
        String countriesFor1Percentage = "cn,in";
        String countriesFor5Percentage = "us,id,br,pk,ng,bd,ru,mx,jp,et,ph,eg,vn,cd,tr,ir,de";
        String countriesFor15Percentage = "th,gb,fr,tz,it,za,mm,ke,kr,co,es,ug,ar,ua,dz,sd,iq";
        String countriesFor25Percentage = "pl,ca,af,ma,sa,pe,uz,ve,my,ao,mz,gh,np,ye,mg,kp,cm";
        String countriesFor35Percentage = "au,tw,ne,lk,bf,mw,ml,ro,kz,sy,cl,zm,gt,zw,nl,ec,sn";
        String countriesFor45Percentage = "kh,td,so,gn,ss,rw,bj,tn,bi,be,cu,bo,ht,gr,do,cz,pt";
        if (countriesFor1Percentage.contains(countryIso)) {
            return 1;
        } else if (countriesFor5Percentage.contains(countryIso)) {
            return 5;
        } else if (countriesFor15Percentage.contains(countryIso)) {
            return 15;
        } else if (countriesFor25Percentage.contains(countryIso)) {
            return 25;
        } else if (countriesFor35Percentage.contains(countryIso)) {
            return 35;
        } else if (countriesFor45Percentage.contains(countryIso)) {
            return 45;
        } else {
            return 50;
        }
    }

    private static int mapSimStateToProto(int simState) {
        switch (simState) {
            case TelephonyManager.SIM_STATE_ABSENT:
                return SimState.SIM_STATE_ABSENT;
            case TelephonyManager.SIM_STATE_LOADED:
                return SimState.SIM_STATE_LOADED;
            default:
                return SimState.SIM_STATE_UNKNOWN;
        }
    }
}
