/*
 * 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 android.telephony.ims.stub;

import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.RemoteException;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.telephony.ims.aidl.IImsSmsListener;
import android.util.Log;

import com.android.internal.telephony.flags.Flags;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.Executor;

/**
 * Base implementation for SMS over IMS.
 *
 * Any service wishing to provide SMS over IMS should extend this class and implement all methods
 * that the service supports.
 *
 * @hide
 */
@SystemApi
public class ImsSmsImplBase {
    private static final String LOG_TAG = "SmsImplBase";

    /** @hide */
    @IntDef({
            SEND_STATUS_OK,
            SEND_STATUS_ERROR,
            SEND_STATUS_ERROR_RETRY,
            SEND_STATUS_ERROR_FALLBACK
        })
    @Retention(RetentionPolicy.SOURCE)
    public @interface SendStatusResult {}
    /**
     * Message was sent successfully.
     */
    public static final int SEND_STATUS_OK = 1;

    /**
     * IMS provider failed to send the message and platform should not retry falling back to sending
     * the message using the radio.
     */
    public static final int SEND_STATUS_ERROR = 2;

    /**
     * IMS provider failed to send the message and platform should retry again after setting TP-RD
     * bit to high.
     */
    public static final int SEND_STATUS_ERROR_RETRY = 3;

    /**
     * IMS provider failed to send the message and platform should retry falling back to sending
     * the message using the radio.
     */
    public static final int SEND_STATUS_ERROR_FALLBACK = 4;

    /** @hide */
    @IntDef({
            DELIVER_STATUS_OK,
            DELIVER_STATUS_ERROR_GENERIC,
            DELIVER_STATUS_ERROR_NO_MEMORY,
            DELIVER_STATUS_ERROR_REQUEST_NOT_SUPPORTED
        })
    @Retention(RetentionPolicy.SOURCE)
    public @interface DeliverStatusResult {}
    /**
     * Message was delivered successfully.
     */
    public static final int DELIVER_STATUS_OK = 1;

    /**
     * Message was not delivered.
     */
    public static final int DELIVER_STATUS_ERROR_GENERIC = 2;

    /**
     * Message was not delivered due to lack of memory.
     */
    public static final int DELIVER_STATUS_ERROR_NO_MEMORY = 3;

    /**
     * Message was not delivered as the request is not supported.
     */
    public static final int DELIVER_STATUS_ERROR_REQUEST_NOT_SUPPORTED = 4;

    /** @hide */
    @IntDef({
            STATUS_REPORT_STATUS_OK,
            STATUS_REPORT_STATUS_ERROR
        })
    @Retention(RetentionPolicy.SOURCE)
    public @interface StatusReportResult {}

    /**
     * Status Report was set successfully.
     */
    public static final int STATUS_REPORT_STATUS_OK = 1;

    /**
     * Error while setting status report.
     */
    public static final int STATUS_REPORT_STATUS_ERROR = 2;

    /**
     * No network error was generated while processing the SMS message.
     */
    // Should match SmsResponse.NO_ERROR_CODE
    public static final int RESULT_NO_NETWORK_ERROR = -1;

    // Lock for feature synchronization
    private final Object mLock = new Object();
    private IImsSmsListener mListener;
    private Executor mExecutor;

    /**
     * Create a new ImsSmsImplBase using the Executor set in MmTelFeature
     */
    public ImsSmsImplBase() {
    }

    /**
     * Create a new ImsSmsImplBase with specified executor.
     * <p>
     * @param executor Default executor for ImsSmsImplBase
     */
    public ImsSmsImplBase(@NonNull Executor executor) {
        mExecutor = executor;
    }

    /**
     * Registers a listener responsible for handling tasks like delivering messages.
     *
     * @param listener listener to register.
     *
     * @hide
     */
    public final void registerSmsListener(IImsSmsListener listener) {
        synchronized (mLock) {
            mListener = listener;
        }
    }

    /**
     * This method will be triggered by the platform when the user attempts to send an SMS. This
     * method should be implemented by the IMS providers to provide implementation of sending an SMS
     * over IMS.
     *
     * @param token unique token generated by the platform that should be used when triggering
     *             callbacks for this specific message.
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     * @param format the format of the message.
     * @param smsc the Short Message Service Center address.
     * @param isRetry whether it is a retry of an already attempted message or not.
     * @param pdu PDU representing the contents of the message.
     */
    public void sendSms(int token, @IntRange(from = 0, to = 65535) int messageRef,
            @SmsMessage.Format String format, String smsc, boolean isRetry,
            byte[] pdu) {
        // Base implementation returns error. Should be overridden.
        try {
            onSendSmsResult(token, messageRef, SEND_STATUS_ERROR,
                    SmsManager.RESULT_ERROR_GENERIC_FAILURE);
        } catch (RuntimeException e) {
            Log.e(LOG_TAG, "Can not send sms: " + e.getMessage());
        }
    }

    /**
     * This method will be triggered by the platform when memory becomes available to receive SMS
     * after a memory full event. This method should be implemented by IMS providers to
     * send RP-SMMA notification from SMS Relay Layer to server over IMS as per section 7.3.2 of
     * TS 124.11. Once the RP-SMMA Notification is sent to the network. The network will deliver all
     * the pending messages which failed due to Unavailability of Memory.
     *
     * @param token unique token generated that should be used when triggering callbacks for this
     * specific message.
     */
    @FlaggedApi(Flags.FLAG_SUPPORT_IMS_SMS_SMMA)
    public void onMemoryAvailable(int token) {
        // Base Implementation - Should be overridden
    }

    /**
     * This method will be triggered by the platform after
     * {@link #onSmsReceived(int, String, byte[])} has been called to deliver the result to the IMS
     * provider.
     *
     * If the framework needs to provide the PDU used to acknowledge the SMS,
     * {@link #acknowledgeSms(int, int, int, byte[])} will be called.
     *
     * @param token token provided in {@link #onSmsReceived(int, String, byte[])}
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     * @param result result of delivering the message.
     */
    public void acknowledgeSms(int token, @IntRange(from = 0, to = 65535)  int messageRef,
            @DeliverStatusResult int result) {
        Log.e(LOG_TAG, "acknowledgeSms() not implemented.");
    }

    /**
     * This method will be called by the platform after
     * {@link #onSmsReceived(int, String, byte[])} has been called to acknowledge an incoming SMS.
     *
     * This method is only called in cases where the framework needs to provide the PDU such as the
     * case where we provide the Short Message Transfer Layer PDU (see 3GPP TS 23.040). Otherwise,
     * {@link #acknowledgeSms(int, int, int)} will be used.
     *
     * @param token token provided in {@link #onSmsReceived(int, String, byte[])}
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     * @param result result of delivering the message.
     * @param pdu PDU representing the contents of the message.
     */
    public void acknowledgeSms(int token, @IntRange(from = 0, to = 65535)  int messageRef,
            @DeliverStatusResult int result, @NonNull byte[] pdu) {
        Log.e(LOG_TAG, "acknowledgeSms() not implemented. acknowledgeSms(int, int, int) called.");
        acknowledgeSms(token, messageRef, result);
    }

    /**
     * This method will be triggered by the platform after
     * {@link #onSmsStatusReportReceived(int, int, String, byte[])} or
     * {@link #onSmsStatusReportReceived(int, String, byte[])} has been called to provide the
     * result to the IMS provider.
     *
     * @param token token provided in {@link #onSmsStatusReportReceived(int, int, String, byte[])}
     *              or {@link #onSmsStatusReportReceived(int, String, byte[])}
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     * @param result result of delivering the message.
     */
    public void acknowledgeSmsReport(int token, @IntRange(from = 0, to = 65535) int messageRef,
            @StatusReportResult int result) {
        Log.e(LOG_TAG, "acknowledgeSmsReport() not implemented.");
    }

    /**
     * This method should be triggered by the IMS providers when there is an incoming message. The
     * platform will deliver the message to the messages database and notify the IMS provider of the
     * result by calling {@link #acknowledgeSms(int, int, int)}.
     *
     * This method must not be called before {@link #onReady()} is called or the call will fail. If
     * the platform is not available, {@link #acknowledgeSms(int, int, int)} will be called with the
     * {@link #DELIVER_STATUS_ERROR_GENERIC} result code.
     * @param token unique token generated by IMS providers that the platform will use to trigger
     *              callbacks for this message.
     * @param format the format of the message.
     * @param pdu PDU representing the contents of the message.
     * @throws RuntimeException if called before {@link #onReady()} is triggered.
     */
    public final void onSmsReceived(int token, @SmsMessage.Format String format, byte[] pdu)
            throws RuntimeException {
        IImsSmsListener listener = null;
        synchronized (mLock) {
            listener = mListener;
        }

        if (listener == null) {
            throw new RuntimeException("Feature not ready.");
        }
        try {
            listener.onSmsReceived(token, format, pdu);
        } catch (RemoteException e) {
            Log.e(LOG_TAG, "Can not deliver sms: " + e.getMessage());
            SmsMessage message = SmsMessage.createFromPdu(pdu, format);
            if (message != null && message.mWrappedSmsMessage != null) {
                acknowledgeSms(token, message.mWrappedSmsMessage.mMessageRef,
                        DELIVER_STATUS_ERROR_GENERIC);
            } else {
                Log.w(LOG_TAG, "onSmsReceived: Invalid pdu entered.");
                acknowledgeSms(token, 0, DELIVER_STATUS_ERROR_GENERIC);
            }
        }
    }

    /**
     * This method should be triggered by the IMS providers when an outgoing SMS message has been
     * sent successfully.
     *
     * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     *
     * @throws RuntimeException if called before {@link #onReady()} is triggered or if the
     * connection to the framework is not available. If this happens attempting to send the SMS
     * should be aborted.
     */
    public final void onSendSmsResultSuccess(int token,
            @IntRange(from = 0, to = 65535) int messageRef) throws RuntimeException {
        IImsSmsListener listener = null;
        synchronized (mLock) {
            listener = mListener;
        }

        if (listener == null) {
            throw new RuntimeException("Feature not ready.");
        }
        try {
            listener.onSendSmsResult(token, messageRef, SEND_STATUS_OK,
                    SmsManager.RESULT_ERROR_NONE, RESULT_NO_NETWORK_ERROR);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * This method should be triggered by the IMS providers to pass the result of the sent message
     * to the platform.
     *
     * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     * @param status result of sending the SMS.
     * @param reason reason in case status is failure.
     *
     * @throws RuntimeException if called before {@link #onReady()} is triggered or if the
     * connection to the framework is not available. If this happens attempting to send the SMS
     * should be aborted.
     * @deprecated Use {@link #onSendSmsResultSuccess(int, int)} or
     * {@link #onSendSmsResultError(int, int, int, int, int)} to notify the framework of the SMS
     * send result.
     */
    @Deprecated
    public final void onSendSmsResult(int token, @IntRange(from = 0, to = 65535) int messageRef,
            @SendStatusResult int status, @SmsManager.Result int reason) throws RuntimeException {
        IImsSmsListener listener = null;
        synchronized (mLock) {
            listener = mListener;
        }

        if (listener == null) {
            throw new RuntimeException("Feature not ready.");
        }
        try {
            listener.onSendSmsResult(token, messageRef, status, reason,
                    RESULT_NO_NETWORK_ERROR);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * This method should be triggered by the IMS providers when an outgoing message fails to be
     * sent due to an error generated while processing the message or after being sent to the
     * network.
     *
     * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format (see TS.123.040) or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     * @param status result of sending the SMS.
     * @param networkErrorCode the error code reported by the carrier network if sending this SMS
     *  has resulted in an error or {@link #RESULT_NO_NETWORK_ERROR} if no network error was
     *  generated. See 3GPP TS 24.011 Section 7.3.4 for valid error codes and more information.
     *
     * @throws RuntimeException if called before {@link #onReady()} is triggered or if the
     * connection to the framework is not available. If this happens attempting to send the SMS
     * should be aborted.
     */
    public final void onSendSmsResultError(int token,
            @IntRange(from = 0, to = 65535) int messageRef, @SendStatusResult int status,
            @SmsManager.Result int reason, int networkErrorCode) throws RuntimeException {
        IImsSmsListener listener = null;
        synchronized (mLock) {
            listener = mListener;
        }

        if (listener == null) {
            throw new RuntimeException("Feature not ready.");
        }
        try {
            listener.onSendSmsResult(token, messageRef, status, reason, networkErrorCode);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * This API is used to report the result of sending
     * RP-SMMA from framework based on received network responses(RP-ACK,
     * RP-ERROR or SIP error).
     *
     * @param token provided in {@link #onMemoryAvailable()}.
     * @param result based on RP-ACK or RP_ERROR
     * @param networkErrorCode the error code reported by the carrier
     * network if sending this SMS has resulted in an error or
     * {@link #RESULT_NO_NETWORK_ERROR} if no network error was generated. See
     * 3GPP TS 24.011 Section 7.3.4 for valid error codes and more
     * information.
     */
    @FlaggedApi(Flags.FLAG_SUPPORT_IMS_SMS_SMMA)
    public final void onMemoryAvailableResult(int token, @SendStatusResult int result,
            int networkErrorCode) throws RuntimeException {
        IImsSmsListener listener = null;
        synchronized (mLock) {
            listener = mListener;
        }

        if (listener == null) {
            throw new RuntimeException("Feature not ready.");
        }
        try {
            listener.onMemoryAvailableResult(token, result, networkErrorCode);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /**
     * This method should be triggered by the IMS providers when the status report of the sent
     * message is received. The platform will handle the report and notify the IMS provider of the
     * result by calling {@link #acknowledgeSmsReport(int, int, int)}.
     *
     * This method must not be called before {@link #onReady()} is called or the call will fail. If
     * the platform is not available, {@link #acknowledgeSmsReport(int, int, int)} will be called
     * with the {@link #STATUS_REPORT_STATUS_ERROR} result code.
     * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
     * @param messageRef the message reference, which may be 1 byte if it is in
     *     {@link SmsMessage#FORMAT_3GPP} format or 2 bytes if it is in
     *     {@link SmsMessage#FORMAT_3GPP2} format (see 3GPP2 C.S0015-B).
     * @param format the format of the message.
     * @param pdu PDU representing the content of the status report.
     * @throws RuntimeException if called before {@link #onReady()} is triggered
     *
     * @deprecated Use {@link #onSmsStatusReportReceived(int, String, byte[])} instead without the
     * message reference.
     */
    @Deprecated
    public final void onSmsStatusReportReceived(int token,
            @IntRange(from = 0, to = 65535) int messageRef, @SmsMessage.Format String format,
            byte[] pdu) throws RuntimeException {
        IImsSmsListener listener = null;
        synchronized (mLock) {
            listener = mListener;
        }

        if (listener == null) {
            throw new RuntimeException("Feature not ready.");
        }
        try {
            listener.onSmsStatusReportReceived(token, format, pdu);
        } catch (RemoteException e) {
            Log.e(LOG_TAG, "Can not process sms status report: " + e.getMessage());
            acknowledgeSmsReport(token, messageRef, STATUS_REPORT_STATUS_ERROR);
        }
    }

    /**
     * This method should be triggered by the IMS providers when the status report of the sent
     * message is received. The platform will handle the report and notify the IMS provider of the
     * result by calling {@link #acknowledgeSmsReport(int, int, int)}.
     *
     * This method must not be called before {@link #onReady()} is called or the call will fail. If
     * the platform is not available, {@link #acknowledgeSmsReport(int, int, int)} will be called
     * with the {@link #STATUS_REPORT_STATUS_ERROR} result code.
     * @param token unique token generated by IMS providers that the platform will use to trigger
     *              callbacks for this message.
     * @param format the format of the message.
     * @param pdu PDU representing the content of the status report.
     * @throws RuntimeException if called before {@link #onReady()} is triggered
     */
    public final void onSmsStatusReportReceived(int token, @SmsMessage.Format String format,
            byte[] pdu) throws RuntimeException {
        IImsSmsListener listener = null;
        synchronized (mLock) {
            listener = mListener;
        }

        if (listener == null) {
            throw new RuntimeException("Feature not ready.");
        }
        try {
            listener.onSmsStatusReportReceived(token, format, pdu);
        } catch (RemoteException e) {
            Log.e(LOG_TAG, "Can not process sms status report: " + e.getMessage());
            SmsMessage message = SmsMessage.createFromPdu(pdu, format);
            if (message != null && message.mWrappedSmsMessage != null) {
                acknowledgeSmsReport(
                        token,
                        message.mWrappedSmsMessage.mMessageRef,
                        STATUS_REPORT_STATUS_ERROR);
            } else {
                Log.w(LOG_TAG, "onSmsStatusReportReceived: Invalid pdu entered.");
                acknowledgeSmsReport(token, 0, STATUS_REPORT_STATUS_ERROR);
            }
        }
    }

    /**
     * Returns the SMS format that the ImsService expects.
     *
     * @return  The expected format of the SMS messages.
     */
    public @SmsMessage.Format String getSmsFormat() {
      return SmsMessage.FORMAT_3GPP;
    }

    /**
     * Called when ImsSmsImpl has been initialized and communication with the framework is set up.
     * Any attempt by this class to access the framework before this method is called will return
     * with a {@link RuntimeException}.
     */
    public void onReady() {
        // Base Implementation - Should be overridden
    }

    /**
     * Set default Executor for ImsSmsImplBase.
     *
     * @param executor The default executor for the framework to use when executing the methods
     * overridden by the implementation of ImsSms.
     * @hide
     */
    public final void setDefaultExecutor(@NonNull Executor executor) {
        if (mExecutor == null) {
            mExecutor = executor;
        }
    }

    /**
     * Get Executor from ImsSmsImplBase.
     * If there is no settings for the executor, all ImsSmsImplBase method calls will use
     * Runnable::run as default
     *
     * @return an Executor used to execute methods in ImsSms called remotely by the framework.
     * @hide
     */
    public @NonNull Executor getExecutor() {
        return mExecutor != null ? mExecutor : Runnable::run;
    }
}
