/*
 * Copyright (C) 2018 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;

import android.app.Activity;
import android.content.Context;
import android.os.Binder;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.provider.Telephony.Sms.Intents;
import android.telephony.CarrierConfigManager;
import android.telephony.ServiceState;
import android.telephony.SmsManager;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.RegistrationManager;
import android.telephony.ims.aidl.IImsSmsListener;
import android.telephony.ims.feature.MmTelFeature;
import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.telephony.ims.stub.ImsSmsImplBase;
import android.telephony.ims.stub.ImsSmsImplBase.SendStatusResult;

import com.android.ims.FeatureConnector;
import com.android.ims.ImsException;
import com.android.ims.ImsManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
import com.android.internal.telephony.analytics.TelephonyAnalytics;
import com.android.internal.telephony.analytics.TelephonyAnalytics.SmsMmsAnalytics;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.uicc.IccUtils;
import com.android.internal.telephony.util.SMSDispatcherUtil;
import com.android.telephony.Rlog;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Responsible for communications with {@link com.android.ims.ImsManager} to send/receive messages
 * over IMS.
 * @hide
 */
public class ImsSmsDispatcher extends SMSDispatcher {

    private static final String TAG = "ImsSmsDispatcher";
    private static final int CONNECT_DELAY_MS = 5000; // 5 seconds;
    public static final int MAX_SEND_RETRIES_OVER_IMS = MAX_SEND_RETRIES;

    /**
     * Creates FeatureConnector instances for ImsManager, used during testing to inject mock
     * connector instances.
     */
    @VisibleForTesting
    public interface FeatureConnectorFactory {
        /**
         * Create a new FeatureConnector for ImsManager.
         */
        FeatureConnector<ImsManager> create(Context context, int phoneId, String logPrefix,
                FeatureConnector.Listener<ImsManager> listener, Executor executor);
    }

    public List<Integer> mMemoryAvailableNotifierList = new ArrayList<Integer>();
    @VisibleForTesting
    public Map<Integer, SmsTracker> mTrackers = new ConcurrentHashMap<>();
    @VisibleForTesting
    public AtomicInteger mNextToken = new AtomicInteger();
    private final Object mLock = new Object();
    private volatile boolean mIsSmsCapable;
    private volatile boolean mIsImsServiceUp;
    private volatile boolean mIsRegistered;
    private final FeatureConnector<ImsManager> mImsManagerConnector;
    /** Telephony metrics instance for logging metrics event */
    private TelephonyMetrics mMetrics = TelephonyMetrics.getInstance();
    private ImsManager mImsManager;
    private FeatureConnectorFactory mConnectorFactory;

    private Runnable mConnectRunnable = new Runnable() {
        @Override
        public void run() {
            mImsManagerConnector.connect();
        }
    };

    /**
     * Listen to the IMS service state change
     *
     */
    private RegistrationManager.RegistrationCallback mRegistrationCallback =
            new RegistrationManager.RegistrationCallback() {
                @Override
                public void onRegistered(
                        @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
                    logd("onImsConnected imsRadioTech=" + imsRadioTech);
                    synchronized (mLock) {
                        mIsRegistered = true;
                    }
                }

                @Override
                public void onRegistering(
                        @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
                    logd("onImsProgressing imsRadioTech=" + imsRadioTech);
                    synchronized (mLock) {
                        mIsRegistered = false;
                    }
                }

                @Override
                public void onUnregistered(ImsReasonInfo info) {
                    logd("onImsDisconnected imsReasonInfo=" + info);
                    synchronized (mLock) {
                        mIsRegistered = false;
                    }
                }
            };

    private android.telephony.ims.ImsMmTelManager.CapabilityCallback mCapabilityCallback =
            new android.telephony.ims.ImsMmTelManager.CapabilityCallback() {
                @Override
                public void onCapabilitiesStatusChanged(
                        MmTelFeature.MmTelCapabilities capabilities) {
                    synchronized (mLock) {
                        mIsSmsCapable = capabilities.isCapable(
                                MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS);
                    }
                }
    };

    private final IImsSmsListener mImsSmsListener = new IImsSmsListener.Stub() {
        @Override
        public void onMemoryAvailableResult(int token, @SendStatusResult int status,
                int networkReasonCode) {
            final long identity = Binder.clearCallingIdentity();
            try {
                logd("onMemoryAvailableResult token=" + token + " status=" + status
                        + " networkReasonCode=" + networkReasonCode);
                if (!mMemoryAvailableNotifierList.contains(token)) {
                    loge("onMemoryAvailableResult Invalid token");
                    return;
                }
                mMemoryAvailableNotifierList.remove(Integer.valueOf(token));

                /**
                 * The Retrans flag is set and reset As per section 6.3.3.1.2 in TS 124011
                 * Note: Assuming that SEND_STATUS_ERROR_RETRY is sent in case of temporary failure
                 */
                if (status ==  ImsSmsImplBase.SEND_STATUS_ERROR_RETRY) {
                    if (!mRPSmmaRetried) {
                        sendMessageDelayed(obtainMessage(EVENT_RETRY_SMMA), SEND_RETRY_DELAY);
                        mRPSmmaRetried = true;
                    } else {
                        mRPSmmaRetried = false;
                    }
                } else {
                    mRPSmmaRetried = false;
                }
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
        @Override
        public void onSendSmsResult(int token, int messageRef, @SendStatusResult int status,
                @SmsManager.Result int reason, int networkReasonCode) {
            final long identity = Binder.clearCallingIdentity();
            try {
                logd("onSendSmsResult token=" + token + " messageRef=" + messageRef
                        + " status=" + status + " reason=" + reason + " networkReasonCode="
                        + networkReasonCode);
                // TODO integrate networkReasonCode into IMS SMS metrics.
                SmsTracker tracker = mTrackers.get(token);
                mMetrics.writeOnImsServiceSmsSolicitedResponse(mPhone.getPhoneId(), status, reason,
                        (tracker != null ? tracker.mMessageId : 0L));
                if (tracker == null) {
                    throw new IllegalArgumentException("Invalid token.");
                }
                tracker.mMessageRef = messageRef;
                switch(status) {
                    case ImsSmsImplBase.SEND_STATUS_OK:
                        if (tracker.mDeliveryIntent != null) {
                            // Expecting a status report. Put this tracker to the map.
                            mSmsDispatchersController.putDeliveryPendingTracker(tracker);
                        }
                        tracker.onSent(mContext);
                        mTrackers.remove(token);
                        mPhone.notifySmsSent(tracker.mDestAddress);
                        mSmsDispatchersController.notifySmsSentToEmergencyStateTracker(
                                tracker.mDestAddress, tracker.mMessageId, true,
                                tracker.isSinglePartOrLastPart());
                        break;
                    case ImsSmsImplBase.SEND_STATUS_ERROR:
                        tracker.onFailed(mContext, reason, networkReasonCode);
                        mTrackers.remove(token);
                        notifySmsSentFailedToEmergencyStateTracker(tracker, true);
                        break;
                    case ImsSmsImplBase.SEND_STATUS_ERROR_RETRY:
                        int maxRetryCountOverIms = getMaxRetryCountOverIms();
                        if (tracker.mRetryCount < getMaxSmsRetryCount()) {
                            if (maxRetryCountOverIms < getMaxSmsRetryCount()
                                    && tracker.mRetryCount >= maxRetryCountOverIms) {
                                tracker.mRetryCount += 1;
                                mTrackers.remove(token);
                                fallbackToPstn(tracker);
                                break;
                            }
                            tracker.mRetryCount += 1;
                            sendMessageDelayed(
                                    obtainMessage(EVENT_SEND_RETRY, tracker),
                                    getSmsRetryDelayValue());
                        } else {
                            tracker.onFailed(mContext, reason, networkReasonCode);
                            mTrackers.remove(token);
                            notifySmsSentFailedToEmergencyStateTracker(tracker, true);
                        }
                        break;
                    case ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK:
                        // Skip MAX_SEND_RETRIES checking here. It allows CSFB after
                        // SEND_STATUS_ERROR_RETRY up to MAX_SEND_RETRIES even.
                        tracker.mRetryCount += 1;
                        mTrackers.remove(token);
                        fallbackToPstn(tracker);
                        break;
                    default:
                }
                mPhone.getSmsStats().onOutgoingSms(
                        true /* isOverIms */,
                        SmsConstants.FORMAT_3GPP2.equals(getFormat()),
                        status == ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK,
                        reason,
                        networkReasonCode,
                        tracker.mMessageId,
                        tracker.isFromDefaultSmsApplication(mContext),
                        tracker.getInterval(),
                        mTelephonyManager.isEmergencyNumber(tracker.mDestAddress));
                if (mPhone != null) {
                    TelephonyAnalytics telephonyAnalytics = mPhone.getTelephonyAnalytics();
                    if (telephonyAnalytics != null) {
                        SmsMmsAnalytics smsMmsAnalytics = telephonyAnalytics.getSmsMmsAnalytics();
                        if (smsMmsAnalytics != null) {
                            smsMmsAnalytics.onOutgoingSms(
                                    true /* isOverIms */,
                                    reason);
                        }
                    }
                }

            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void onSmsStatusReportReceived(int token, String format, byte[] pdu)
                throws RemoteException {
            final long identity = Binder.clearCallingIdentity();
            try {
                logd("Status report received.");
                android.telephony.SmsMessage message =
                        android.telephony.SmsMessage.createFromPdu(pdu, format);
                if (message == null || message.mWrappedSmsMessage == null) {
                    throw new RemoteException(
                            "Status report received with a PDU that could not be parsed.");
                }
                mSmsDispatchersController.handleSmsStatusReport(format, pdu);
                try {
                    getImsManager().acknowledgeSmsReport(
                            token,
                            message.mWrappedSmsMessage.mMessageRef,
                            ImsSmsImplBase.STATUS_REPORT_STATUS_OK);
                } catch (ImsException e) {
                    loge("Failed to acknowledgeSmsReport(). Error: " + e.getMessage());
                }
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }

        @Override
        public void onSmsReceived(int token, String format, byte[] pdu) {
            final long identity = Binder.clearCallingIdentity();
            try {
                logd("SMS received.");
                android.telephony.SmsMessage message =
                        android.telephony.SmsMessage.createFromPdu(pdu, format);
                mSmsDispatchersController.injectSmsPdu(message, format, result -> {
                    logd("SMS handled result: " + result);
                    int mappedResult;
                    switch (result) {
                        case Intents.RESULT_SMS_HANDLED:
                            mappedResult = ImsSmsImplBase.DELIVER_STATUS_OK;
                            if (message != null) {
                                mSmsDispatchersController
                                        .notifySmsReceivedViaImsToEmergencyStateTracker(
                                                message.getOriginatingAddress());
                            }
                            break;
                        case Intents.RESULT_SMS_OUT_OF_MEMORY:
                            mappedResult = ImsSmsImplBase.DELIVER_STATUS_ERROR_NO_MEMORY;
                            break;
                        case Intents.RESULT_SMS_UNSUPPORTED:
                            mappedResult =
                                    ImsSmsImplBase.DELIVER_STATUS_ERROR_REQUEST_NOT_SUPPORTED;
                            break;
                        case Activity.RESULT_OK:
                            // class2 message saving to SIM operation is in progress, defer ack
                            // until saving to SIM is success/failure
                            return;
                        default:
                            mappedResult = ImsSmsImplBase.DELIVER_STATUS_ERROR_GENERIC;
                            break;
                    }
                    try {
                        if (message != null && message.mWrappedSmsMessage != null) {
                            getImsManager().acknowledgeSms(token,
                                    message.mWrappedSmsMessage.mMessageRef, mappedResult);
                        } else {
                            logw("SMS Received with a PDU that could not be parsed.");
                            getImsManager().acknowledgeSms(token, 0, mappedResult);
                        }
                    } catch (ImsException e) {
                        loge("Failed to acknowledgeSms(). Error: " + e.getMessage());
                    }
                }, true /* ignoreClass */, true /* isOverIms */, token);
            } finally {
                Binder.restoreCallingIdentity(identity);
            }
        }
    };

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case EVENT_SEND_RETRY:
                logd("SMS retry..");
                sendSms((SmsTracker) msg.obj);
                break;
            case EVENT_RETRY_SMMA:
                logd("SMMA Notification retry..");
                onMemoryAvailable();
                break;
            default:
                super.handleMessage(msg);
        }
    }

    public ImsSmsDispatcher(Phone phone, SmsDispatchersController smsDispatchersController,
            FeatureConnectorFactory factory) {
        super(phone, smsDispatchersController);
        mConnectorFactory = factory;

        mImsManagerConnector = mConnectorFactory.create(mContext, mPhone.getPhoneId(), TAG,
                new FeatureConnector.Listener<ImsManager>() {
                    public void connectionReady(ImsManager manager, int subId) throws ImsException {
                        logd("ImsManager: connection ready.");
                        synchronized (mLock) {
                            mImsManager = manager;
                            setListeners();
                            mIsImsServiceUp = true;

                            /* set ImsManager */
                            mSmsDispatchersController.setImsManager(mImsManager);
                        }
                    }

                    @Override
                    public void connectionUnavailable(int reason) {
                        logd("ImsManager: connection unavailable, reason=" + reason);
                        if (reason == FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE) {
                            loge("connectionUnavailable: unexpected, received server error");
                            removeCallbacks(mConnectRunnable);
                            postDelayed(mConnectRunnable, CONNECT_DELAY_MS);
                        }
                        synchronized (mLock) {
                            mImsManager = null;
                            mIsImsServiceUp = false;

                            /* unset ImsManager */
                            mSmsDispatchersController.setImsManager(null);
                        }
                    }
                }, this::post);
        post(mConnectRunnable);
    }

    private void setListeners() throws ImsException {
        getImsManager().addRegistrationCallback(mRegistrationCallback, this::post);
        getImsManager().addCapabilitiesCallback(mCapabilityCallback, this::post);
        getImsManager().setSmsListener(getSmsListener());
        getImsManager().onSmsReady();
    }

    private boolean isLteService() {
        return ((mPhone.getServiceState().getRilDataRadioTechnology() ==
            ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && (mPhone.getServiceState().
                getDataRegistrationState() == ServiceState.STATE_IN_SERVICE));
    }

    private boolean isLimitedLteService() {
        return ((mPhone.getServiceState().getRilVoiceRadioTechnology() ==
            ServiceState.RIL_RADIO_TECHNOLOGY_LTE) && mPhone.getServiceState().isEmergencyOnly());
    }

    private boolean isEmergencySmsPossible() {
        return isLteService() || isLimitedLteService();
    }

    public boolean isEmergencySmsSupport(String destAddr) {
        PersistableBundle b;
        boolean eSmsCarrierSupport = false;
        if (!mTelephonyManager.isEmergencyNumber(destAddr)) {
            logi(Rlog.pii(TAG, destAddr) + " is not emergency number");
            return false;
        }

        final long identity = Binder.clearCallingIdentity();
        try {
            CarrierConfigManager configManager = (CarrierConfigManager) mContext
                    .getSystemService(Context.CARRIER_CONFIG_SERVICE);
            if (configManager == null) {
                loge("configManager is null");
                return false;
            }
            b = configManager.getConfigForSubId(getSubId());
            if (b == null) {
                loge("PersistableBundle is null");
                return false;
            }
            eSmsCarrierSupport = b.getBoolean(
                    CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL);
            boolean lteOrLimitedLte = isEmergencySmsPossible();
            logi("isEmergencySmsSupport emergencySmsCarrierSupport: "
                    + eSmsCarrierSupport + " destAddr: " + Rlog.pii(TAG, destAddr)
                    + " mIsImsServiceUp: " + mIsImsServiceUp + " lteOrLimitedLte: "
                    + lteOrLimitedLte);

            return eSmsCarrierSupport && mIsImsServiceUp && lteOrLimitedLte;
        } finally {
            Binder.restoreCallingIdentity(identity);
        }
    }

    public boolean isAvailable() {
        synchronized (mLock) {
            logd("isAvailable: up=" + mIsImsServiceUp + ", reg= " + mIsRegistered
                    + ", cap= " + mIsSmsCapable);
            return mIsImsServiceUp && mIsRegistered && mIsSmsCapable;
        }
    }

    @Override
    protected String getFormat() {
        // This is called in the constructor before ImsSmsDispatcher has a chance to initialize
        // mLock. ImsManager will not be up anyway at this point, so report UNKNOWN.
        if (mLock == null) return SmsConstants.FORMAT_UNKNOWN;
        try {
            return getImsManager().getSmsFormat();
        } catch (ImsException e) {
            loge("Failed to get sms format. Error: " + e.getMessage());
            return SmsConstants.FORMAT_UNKNOWN;
        }
    }

    @Override
    public int getMaxSmsRetryCount() {
        int retryCount = MAX_SEND_RETRIES;
        CarrierConfigManager mConfigManager;

        mConfigManager = (CarrierConfigManager)  mContext.getSystemService(
                Context.CARRIER_CONFIG_SERVICE);

        if (mConfigManager != null) {
            PersistableBundle carrierConfig = mConfigManager.getConfigForSubId(
                    getSubId());

            if (carrierConfig != null) {
                retryCount = carrierConfig.getInt(
                        CarrierConfigManager.ImsSms.KEY_SMS_MAX_RETRY_COUNT_INT);
            }
        }

        Rlog.d(TAG, "Retry Count: " + retryCount);

        return retryCount;
    }

    /**
     * Returns the number of times SMS can be sent over IMS
     *
     * @return  retry count over Ims from  carrier configuration
     */
    @VisibleForTesting
    public int getMaxRetryCountOverIms() {
        int retryCountOverIms = MAX_SEND_RETRIES_OVER_IMS;
        CarrierConfigManager mConfigManager;

        mConfigManager = (CarrierConfigManager) mContext.getSystemService(Context
                                                        .CARRIER_CONFIG_SERVICE);

        if (mConfigManager != null) {
            PersistableBundle carrierConfig = mConfigManager.getConfigForSubId(
                    getSubId());


            if (carrierConfig != null) {
                retryCountOverIms = carrierConfig.getInt(
                        CarrierConfigManager.ImsSms.KEY_SMS_MAX_RETRY_OVER_IMS_COUNT_INT);
            }
        }

        Rlog.d(TAG, "Retry Count Over Ims: " + retryCountOverIms);

        return retryCountOverIms;
    }

    @Override
    public int getSmsRetryDelayValue() {
        int retryDelay = SEND_RETRY_DELAY;
        CarrierConfigManager mConfigManager;

        mConfigManager = (CarrierConfigManager)  mContext.getSystemService(
                Context.CARRIER_CONFIG_SERVICE);

        if (mConfigManager != null) {
            PersistableBundle carrierConfig = mConfigManager.getConfigForSubId(
                    getSubId());

            if (carrierConfig != null) {
                retryDelay = carrierConfig.getInt(
                        CarrierConfigManager.ImsSms.KEY_SMS_OVER_IMS_SEND_RETRY_DELAY_MILLIS_INT);
            }
        }

        Rlog.d(TAG, "Retry delay: " + retryDelay);

        return retryDelay;
    }

    @Override
    protected boolean shouldBlockSmsForEcbm() {
        // We should not block outgoing SMS during ECM on IMS. It only applies to outgoing CDMA
        // SMS.
        return false;
    }

    @Override
    protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
            String message, boolean statusReportRequested, SmsHeader smsHeader, int priority,
            int validityPeriod) {
        return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, message,
                statusReportRequested, smsHeader, priority, validityPeriod);
    }

    @Override
    protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
            int destPort, byte[] message, boolean statusReportRequested) {
        return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, destPort, message,
                statusReportRequested);
    }

    @Override
    protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
            String message, boolean statusReportRequested, SmsHeader smsHeader, int priority,
            int validityPeriod, int messageRef) {
        return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, message,
                statusReportRequested, smsHeader, priority, validityPeriod, messageRef);
    }

    @Override
    protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
            int destPort, byte[] message, boolean statusReportRequested, int messageRef) {
        return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, destPort, message,
                statusReportRequested, messageRef);
    }

    @Override
    protected TextEncodingDetails calculateLength(CharSequence messageBody, boolean use7bitOnly) {
        return SMSDispatcherUtil.calculateLength(isCdmaMo(), messageBody, use7bitOnly);
    }

    /**
     * Send the Memory Available Event to the ImsService
     */
    public void onMemoryAvailable() {
        logd("onMemoryAvailable ");
        int token = mNextToken.incrementAndGet();
        try {
            logd("onMemoryAvailable: token = " + token);
            mMemoryAvailableNotifierList.add(token);
            getImsManager().onMemoryAvailable(token);
        } catch (ImsException e) {
            loge("onMemoryAvailable failed: " + e.getMessage());
            if (mMemoryAvailableNotifierList.contains(token)) {
                mMemoryAvailableNotifierList.remove(Integer.valueOf(token));
            }
        }
    }

    @Override
    public void sendSms(SmsTracker tracker) {
        logd("sendSms: "
                + " mRetryCount=" + tracker.mRetryCount
                + " mMessageRef=" + tracker.mMessageRef
                + " SS=" + mPhone.getServiceState().getState());

        // Flag that this Tracker is using the ImsService implementation of SMS over IMS for sending
        // this message. Any fallbacks will happen over CS only.
        tracker.mUsesImsServiceForIms = true;

        HashMap<String, Object> map = tracker.getData();

        byte[] pdu = (byte[]) map.get(MAP_KEY_PDU);
        byte smsc[] = (byte[]) map.get(MAP_KEY_SMSC);
        boolean isRetry = tracker.mRetryCount > 0;
        String format = getFormat();

        if (SmsConstants.FORMAT_3GPP.equals(format) && isRetry) {
            // per TS 23.040 Section 9.2.3.6:  If TP-MTI SMS-SUBMIT (0x01) type
            //   TP-RD (bit 2) is 1 for retry
            //   and TP-MR is set to previously failed sms TP-MR
            if (((0x01 & pdu[0]) == 0x01)) {
                pdu[0] |= 0x04; // TP-RD
                pdu[1] = (byte) tracker.mMessageRef; // TP-MR
            }
        }

        int token = mNextToken.incrementAndGet();
        mTrackers.put(token, tracker);
        try {
            getImsManager().sendSms(
                    token,
                    tracker.mMessageRef,
                    format,
                    smsc != null ? IccUtils.bytesToHexString(smsc) : null,
                    isRetry,
                    pdu);
            mMetrics.writeImsServiceSendSms(mPhone.getPhoneId(), format,
                    ImsSmsImplBase.SEND_STATUS_OK, tracker.mMessageId);
        } catch (ImsException e) {
            loge("sendSms failed. Falling back to PSTN. Error: " + e.getMessage());
            mTrackers.remove(token);
            fallbackToPstn(tracker);
            mMetrics.writeImsServiceSendSms(mPhone.getPhoneId(), format,
                    ImsSmsImplBase.SEND_STATUS_ERROR_FALLBACK, tracker.mMessageId);
            mPhone.getSmsStats().onOutgoingSms(
                    true /* isOverIms */,
                    SmsConstants.FORMAT_3GPP2.equals(format),
                    true /* fallbackToCs */,
                    SmsManager.RESULT_SYSTEM_ERROR,
                    tracker.mMessageId,
                    tracker.isFromDefaultSmsApplication(mContext),
                    tracker.getInterval(),
                    mTelephonyManager.isEmergencyNumber(tracker.mDestAddress));
            if (mPhone != null) {
                TelephonyAnalytics telephonyAnalytics = mPhone.getTelephonyAnalytics();
                if (telephonyAnalytics != null) {
                    SmsMmsAnalytics smsMmsAnalytics = telephonyAnalytics.getSmsMmsAnalytics();
                    if (smsMmsAnalytics != null) {
                        smsMmsAnalytics.onOutgoingSms(
                                true /* isOverIms */,
                                SmsManager.RESULT_SYSTEM_ERROR
                        );
                    }
                }
            }
        }
    }

    private ImsManager getImsManager() throws ImsException {
        synchronized (mLock) {
            if (mImsManager == null) {
                throw new ImsException("ImsManager not up",
                        ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
            }
            return mImsManager;
        }
    }

    @VisibleForTesting
    public void fallbackToPstn(SmsTracker tracker) {
        mSmsDispatchersController.sendRetrySms(tracker);
    }

    @Override
    protected boolean isCdmaMo() {
        return mSmsDispatchersController.isCdmaFormat(getFormat());
    }

    @VisibleForTesting
    public IImsSmsListener getSmsListener() {
        return mImsSmsListener;
    }

    private void logd(String s) {
        Rlog.d(TAG + " [" + getPhoneId(mPhone) + "]", s);
    }

    private void logi(String s) {
        Rlog.i(TAG + " [" + getPhoneId(mPhone) + "]", s);
    }

    private void logw(String s) {
        Rlog.w(TAG + " [" + getPhoneId(mPhone) + "]", s);
    }

    private void loge(String s) {
        Rlog.e(TAG + " [" + getPhoneId(mPhone) + "]", s);
    }

    private static String getPhoneId(Phone phone) {
        return (phone != null) ? Integer.toString(phone.getPhoneId()) : "?";
    }
}
