package com.android.bluetooth.sap;

import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothSap;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Icon;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.telephony.TelephonyManager;
import android.util.Log;

import com.android.bluetooth.R;
import com.android.bluetooth.Utils;
import com.android.internal.annotations.VisibleForTesting;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;


/**
 * The SapServer uses two threads, one for reading messages from the RFCOMM socket and
 * one for writing the responses.
 * Incoming requests are decoded in the "main" SapServer, by the decoder build into SapMEssage.
 * The relevant RIL calls are made from the message handler thread through the rild-bt socket.
 * The RIL replies are read in the SapRilReceiver, and passed to the SapServer message handler
 * to be written to the RFCOMM socket.
 * All writes to the RFCOMM and rild-bt socket must be synchronized, hence to write e.g. an error
 * response, send a message to the Sap Handler thread. (There are helper functions to do this)
 * Communication to the RIL is through an intent, and a BroadcastReceiver.
 */
public class SapServer extends Thread implements Callback {
    private static final String TAG = "SapServer";
    private static final String TAG_HANDLER = "SapServerHandler";
    public static final boolean DEBUG = SapService.DEBUG;
    public static final boolean VERBOSE = SapService.VERBOSE;

    @VisibleForTesting
    enum SAP_STATE {
        DISCONNECTED, CONNECTING, CONNECTING_CALL_ONGOING, CONNECTED, CONNECTED_BUSY, DISCONNECTING;
    }

    @VisibleForTesting
    SAP_STATE mState = SAP_STATE.DISCONNECTED;

    private Context mContext = null;
    /* RFCOMM socket I/O streams */
    private BufferedOutputStream mRfcommOut = null;
    private BufferedInputStream mRfcommIn = null;
    /* References to the SapRilReceiver object */
    @VisibleForTesting
    ISapRilReceiver mRilBtReceiver = null;
    /* The message handler members */
    @VisibleForTesting
    Handler mSapHandler = null;
    private HandlerThread mHandlerThread = null;
    /* Reference to the SAP service - which created this instance of the SAP server */
    private Handler mSapServiceHandler = null;

    /* flag for when user forces disconnect of rfcomm */
    @VisibleForTesting
    boolean mIsLocalInitDisconnect = false;
    private CountDownLatch mDeinitSignal = new CountDownLatch(1);

    /* Message ID's handled by the message handler */
    public static final int SAP_MSG_RFC_REPLY = 0x00;
    public static final int SAP_MSG_RIL_CONNECT = 0x01;
    public static final int SAP_MSG_RIL_REQ = 0x02;
    public static final int SAP_MSG_RIL_IND = 0x03;
    public static final int SAP_RIL_SOCK_CLOSED = 0x04;
    public static final int SAP_PROXY_DEAD = 0x05;

    public static final String SAP_DISCONNECT_ACTION =
            "com.android.bluetooth.sap.action.DISCONNECT_ACTION";
    public static final String SAP_DISCONNECT_TYPE_EXTRA =
            "com.android.bluetooth.sap.extra.DISCONNECT_TYPE";
    public static final int NOTIFICATION_ID = android.R.drawable.stat_sys_data_bluetooth;
    @VisibleForTesting
    static final String SAP_NOTIFICATION_CHANNEL = "sap_notification_channel";
    public static final int ISAP_GET_SERVICE_DELAY_MILLIS = 3 * 1000;
    private static final int DISCONNECT_TIMEOUT_IMMEDIATE = 5000; /* ms */
    private static final int DISCONNECT_TIMEOUT_RFCOMM = 2000; /* ms */
    private PendingIntent mPendingDiscIntent = null;
    // Holds a reference to disconnect timeout intents

    /* Timeout for the message handler thread join, to prevent potential ANR. */
    private static final int HANDLER_THREAD_JOIN_TIMEOUT_MS = 1000;

    /* We store the mMaxMessageSize, as we need a copy of it when the init. sequence completes */
    private int mMaxMsgSize = 0;
    /* keep track of the current RIL test mode */
    @VisibleForTesting
    int mTestMode = SapMessage.INVALID_VALUE; // used to set the RIL in test mode

    /**
     * SapServer constructor
     *
     * @param serviceHandler The handler to send a SapService.MSG_SERVERSESSION_CLOSE when closing
     * @param inStream       The socket input stream
     * @param outStream      The socket output stream
     */
    public SapServer(Handler serviceHandler, Context context, InputStream inStream,
            OutputStream outStream) {
        mContext = context;
        mSapServiceHandler = serviceHandler;

        /* Open in- and output streams */
        mRfcommIn = new BufferedInputStream(inStream);
        mRfcommOut = new BufferedOutputStream(outStream);

        /* Register for phone state change and the RIL cfm message */
        IntentFilter filter = new IntentFilter();
        filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
        filter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
        filter.addAction(SAP_DISCONNECT_ACTION);
        mIntentReceiver = new SapServerBroadcastReceiver();
        mContext.registerReceiver(mIntentReceiver, filter);
    }

    /**
     * This handles the response from RIL.
     */
    @VisibleForTesting
    BroadcastReceiver mIntentReceiver;

    @VisibleForTesting
    class SapServerBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(TelephonyManager.ACTION_PHONE_STATE_CHANGED)) {
                if (VERBOSE) {
                    Log.i(TAG,
                            "ACTION_PHONE_STATE_CHANGED intent received in state " + mState.name()
                                    + "PhoneState: " + intent.getStringExtra(
                                    TelephonyManager.EXTRA_STATE));
                }
                if (mState == SAP_STATE.CONNECTING_CALL_ONGOING) {
                    String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
                    if (state != null) {
                        if (state.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
                            if (DEBUG) {
                                Log.i(TAG, "sending RIL.ACTION_RIL_RECONNECT_OFF_REQ intent");
                            }
                            SapMessage fakeConReq = new SapMessage(SapMessage.ID_CONNECT_REQ);
                            fakeConReq.setMaxMsgSize(mMaxMsgSize);
                            onConnectRequest(fakeConReq);
                        }
                    }
                }
            } else if (intent.getAction().equals(SAP_DISCONNECT_ACTION)) {
                int disconnectType = intent.getIntExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA,
                        SapMessage.DISC_GRACEFULL);
                Log.v(TAG, " - Received SAP_DISCONNECT_ACTION type: " + disconnectType);

                if (disconnectType == SapMessage.DISC_RFCOMM) {
                    // At timeout we need to close the RFCOMM socket to complete shutdown
                    shutdown();
                } else if (mState != SAP_STATE.DISCONNECTED && mState != SAP_STATE.DISCONNECTING) {
                    // The user pressed disconnect - initiate disconnect sequence.
                    sendDisconnectInd(disconnectType);
                }
            } else {
                Log.w(TAG, "RIL-BT received unexpected Intent: " + intent.getAction());
            }
        }
    }

    /**
     * Set RIL driver in test mode - only possible if SapMessage is build with TEST == true
     * The value set by this function will take effect at the next connect request received
     * in DISCONNECTED state.
     *
     * @param testMode Use SapMessage.TEST_MODE_XXX
     */
    public void setTestMode(int testMode) {
        if (SapMessage.TEST || Utils.isInstrumentationTestMode()) {
            mTestMode = testMode;
        }
    }

    @VisibleForTesting
    void sendDisconnectInd(int discType) {
        if (VERBOSE) {
            Log.v(TAG, "in sendDisconnectInd()");
        }

        if (discType != SapMessage.DISC_FORCED) {
            if (VERBOSE) {
                Log.d(TAG, "Sending  disconnect (" + discType + ") indication to client");
            }
            /* Send disconnect to client */
            SapMessage discInd = new SapMessage(SapMessage.ID_DISCONNECT_IND);
            discInd.setDisconnectionType(discType);
            sendClientMessage(discInd);

            /* Handle local disconnect procedures */
            if (discType == SapMessage.DISC_GRACEFULL) {
                /* Update the notification to allow the user to initiate a force disconnect */
                setNotification(SapMessage.DISC_IMMEDIATE,
                        PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);

            } else if (discType == SapMessage.DISC_IMMEDIATE) {
                /* Request an immediate disconnect, but start a timer to force disconnect if the
                 * client do not obey our request. */
                startDisconnectTimer(SapMessage.DISC_FORCED, DISCONNECT_TIMEOUT_IMMEDIATE);
            }

        } else {
            SapMessage msg = new SapMessage(SapMessage.ID_DISCONNECT_REQ);
            /* Force disconnect of RFCOMM - but first we need to clean up. */
            clearPendingRilResponses(msg);

            /* We simply need to forward to RIL, but not change state to busy - hence send and set
               message to null. */
            changeState(SAP_STATE.DISCONNECTING);
            sendRilThreadMessage(msg);
            mIsLocalInitDisconnect = true;
        }
    }

    void setNotification(int type, int flags) {
        String title, text, button, ticker;
        Notification notification;
        NotificationManager notificationManager =
                mContext.getSystemService(NotificationManager.class);
        NotificationChannel notificationChannel = new NotificationChannel(SAP_NOTIFICATION_CHANNEL,
                mContext.getString(R.string.bluetooth_sap_notif_title),
                NotificationManager.IMPORTANCE_HIGH);
        notificationManager.createNotificationChannel(notificationChannel);
        flags |= PendingIntent.FLAG_IMMUTABLE;
        if (VERBOSE) {
            Log.i(TAG, "setNotification type: " + type);
        }
        /* For PTS TC_SERVER_DCN_BV_03_I we need to expose the option to send immediate disconnect
         * without first sending a graceful disconnect.
         * To enable this option set
         * bt.sap.pts="true" */
        String ptsEnabled = SystemProperties.get("bt.sap.pts");
        Boolean ptsTest = Boolean.parseBoolean(ptsEnabled);

        /* put notification up for the user to be able to disconnect from the client*/
        Intent sapDisconnectIntent = new Intent(SapServer.SAP_DISCONNECT_ACTION);
        if (type == SapMessage.DISC_GRACEFULL) {
            title = mContext.getString(R.string.bluetooth_sap_notif_title);
            button = mContext.getString(R.string.bluetooth_sap_notif_disconnect_button);
            text = mContext.getString(R.string.bluetooth_sap_notif_message);
            ticker = mContext.getString(R.string.bluetooth_sap_notif_ticker);
        } else {
            title = mContext.getString(R.string.bluetooth_sap_notif_title);
            button = mContext.getString(R.string.bluetooth_sap_notif_force_disconnect_button);
            text = mContext.getString(R.string.bluetooth_sap_notif_disconnecting);
            ticker = mContext.getString(R.string.bluetooth_sap_notif_ticker);
        }
        if (!ptsTest) {
            sapDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA, type);
            PendingIntent pIntentDisconnect =
                    PendingIntent.getBroadcast(mContext, type, sapDisconnectIntent, flags);
            Notification.Action actionDisconnect =
                    new Notification.Action.Builder(Icon.createWithResource(mContext,
                            android.R.drawable.stat_sys_data_bluetooth), button,
                            pIntentDisconnect).build();
            notification =
                    new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true)
                            .addAction(actionDisconnect)
                            .setContentTitle(title)
                            .setTicker(ticker)
                            .setContentText(text)
                            .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
                            .setAutoCancel(false)
                            .setOnlyAlertOnce(true)
                            .setLocalOnly(true)
                            .build();
        } else {
            sapDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA,
                    SapMessage.DISC_GRACEFULL);
            Intent sapForceDisconnectIntent = new Intent(SapServer.SAP_DISCONNECT_ACTION);
            sapForceDisconnectIntent.putExtra(SapServer.SAP_DISCONNECT_TYPE_EXTRA,
                    SapMessage.DISC_IMMEDIATE);
            PendingIntent pIntentDisconnect =
                    PendingIntent.getBroadcast(mContext, SapMessage.DISC_GRACEFULL,
                            sapDisconnectIntent, flags);
            PendingIntent pIntentForceDisconnect =
                    PendingIntent.getBroadcast(mContext, SapMessage.DISC_IMMEDIATE,
                            sapForceDisconnectIntent, flags);
            Notification.Action actionDisconnect = new Notification.Action.Builder(
                    Icon.createWithResource(mContext, android.R.drawable.stat_sys_data_bluetooth),
                    mContext.getString(R.string.bluetooth_sap_notif_disconnect_button),
                    pIntentDisconnect).build();
            Notification.Action actionForceDisconnect =
                    new Notification.Action.Builder(Icon.createWithResource(mContext,
                            android.R.drawable.stat_sys_data_bluetooth),
                            mContext.getString(
                                    R.string.bluetooth_sap_notif_force_disconnect_button),
                            pIntentForceDisconnect).build();
            notification =
                    new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true)
                            .addAction(actionDisconnect)
                            .addAction(actionForceDisconnect)
                            .setContentTitle(title)
                            .setTicker(ticker)
                            .setContentText(text)
                            .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth)
                            .setAutoCancel(false)
                            .setOnlyAlertOnce(true)
                            .setLocalOnly(true)
                            .build();
        }

        // cannot be set with the builder
        notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONLY_ALERT_ONCE;

        notificationManager.notify(NOTIFICATION_ID, notification);
    }

    void clearNotification() {
        NotificationManager notificationManager =
                mContext.getSystemService(NotificationManager.class);
        notificationManager.cancel(SapServer.NOTIFICATION_ID);
    }

    /**
     * The SapServer RFCOMM reader thread. Sets up the handler thread and handle
     * all read from the RFCOMM in-socket. This thread also handle writes to the RIL socket.
     */
    @Override
    public void run() {
        try {
            /* SAP is not time critical, hence lowering priority to ensure critical tasks are
             * executed in a timely manner. */
            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);

            /* Start the SAP message handler thread */
            mHandlerThread = new HandlerThread("SapServerHandler",
                    android.os.Process.THREAD_PRIORITY_BACKGROUND);
            mHandlerThread.start();

            // This will return when the looper is ready
            Looper sapLooper = mHandlerThread.getLooper();
            mSapHandler = new Handler(sapLooper, this);

            if (SapRilReceiver.isAidlSupported()) {
                mRilBtReceiver = new SapRilReceiver(mSapHandler, mSapServiceHandler);
            } else {
                mRilBtReceiver = new SapRilReceiverHidl(mSapHandler, mSapServiceHandler);
            }
            boolean done = false;
            while (!done) {
                if (VERBOSE) {
                    Log.i(TAG, "Waiting for incomming RFCOMM message...");
                }
                int requestType = mRfcommIn.read();
                if (VERBOSE) {
                    Log.i(TAG, "RFCOMM message read...");
                }
                if (requestType == -1) {
                    if (VERBOSE) {
                        Log.i(TAG, "requestType == -1");
                    }
                    done = true; // EOF reached
                } else {
                    if (VERBOSE) {
                        Log.i(TAG, "requestType != -1");
                    }
                    SapMessage msg = SapMessage.readMessage(requestType, mRfcommIn);
                    /* notify about an incoming message from the BT Client */
                    SapService.notifyUpdateWakeLock(mSapServiceHandler);
                    if (msg != null && mState != SAP_STATE.DISCONNECTING) {
                        switch (requestType) {
                            case SapMessage.ID_CONNECT_REQ:
                                if (VERBOSE) {
                                    Log.d(TAG, "CONNECT_REQ - MaxMsgSize: " + msg.getMaxMsgSize());
                                }
                                onConnectRequest(msg);
                                msg = null; /* don't send ril connect yet */
                                break;
                            case SapMessage.ID_DISCONNECT_REQ: /* No params */
                                /*
                                 * 1) send RIL_REQUEST_SIM_SAP_DISCONNECT
                                 *      (block for all incoming requests, as they are not
                                 *       allowed, don't even send an error_resp)
                                 * 2) on response disconnect ril socket.
                                 * 3) when disconnected send RIL.ACTION_RIL_RECONNECT_OFF_REQ
                                 * 4) on RIL.ACTION_RIL_RECONNECT_CFM
                                 *       send SAP_DISCONNECT_RESP to client.
                                 * 5) Start RFCOMM disconnect timer
                                 * 6.a) on rfcomm disconnect:
                                 *       cancel timer and initiate cleanup
                                 * 6.b) on rfcomm disc. timeout:
                                 *       close socket-streams and initiate cleanup */
                                if (VERBOSE) {
                                    Log.d(TAG, "DISCONNECT_REQ");
                                }

                                if (mState == SAP_STATE.CONNECTING_CALL_ONGOING) {
                                    Log.d(TAG, "disconnect received when call was ongoing, "
                                            + "send disconnect response");
                                    changeState(SAP_STATE.DISCONNECTING);
                                    SapMessage reply =
                                            new SapMessage(SapMessage.ID_DISCONNECT_RESP);
                                    sendClientMessage(reply);
                                } else {
                                    clearPendingRilResponses(msg);
                                    changeState(SAP_STATE.DISCONNECTING);
                                    sendRilThreadMessage(msg);
                                    /*cancel the timer for the hard-disconnect intent*/
                                    stopDisconnectTimer();
                                }
                                msg = null; // No message needs to be sent to RIL
                                break;
                            case SapMessage.ID_POWER_SIM_OFF_REQ: // Fall through
                            case SapMessage.ID_RESET_SIM_REQ:
                                /* Forward these to the RIL regardless of the state, and clear any
                                 * pending resp */
                                clearPendingRilResponses(msg);
                                break;
                            case SapMessage.ID_SET_TRANSPORT_PROTOCOL_REQ:
                                /* The RIL might support more protocols that specified in the SAP,
                                 * allow only the valid values. */
                                if (mState == SAP_STATE.CONNECTED && msg.getTransportProtocol() != 0
                                        && msg.getTransportProtocol() != 1) {
                                    Log.w(TAG, "Invalid TransportProtocol received:"
                                            + msg.getTransportProtocol());
                                    // We shall only handle one request at the time, hence return
                                    // error
                                    SapMessage errorReply =
                                            new SapMessage(SapMessage.ID_ERROR_RESP);
                                    sendClientMessage(errorReply);
                                    msg = null;
                                }
                                // Fall through
                            default:
                                /* Remaining cases just needs to be forwarded to the RIL unless
                                we are
                                 * in busy state. */
                                if (mState != SAP_STATE.CONNECTED) {
                                    Log.w(TAG, "Message received in STATE != CONNECTED - state = "
                                            + mState.name());
                                    // We shall only handle one request at the time, hence return
                                    // error
                                    SapMessage errorReply =
                                            new SapMessage(SapMessage.ID_ERROR_RESP);
                                    sendClientMessage(errorReply);
                                    msg = null;
                                }
                        }

                        if (msg != null && msg.getSendToRil()) {
                            changeState(SAP_STATE.CONNECTED_BUSY);
                            sendRilThreadMessage(msg);
                        }

                    } else {
                        //An unknown message or in disconnecting state - send error indication
                        Log.e(TAG, "Unable to parse message.");
                        SapMessage atrReply = new SapMessage(SapMessage.ID_ERROR_RESP);
                        sendClientMessage(atrReply);
                    }
                }
            } // end while
        } catch (NullPointerException e) {
            Log.w(TAG, e);
        } catch (IOException e) {
            /* This is expected during shutdown */
            Log.i(TAG, "IOException received, this is probably a shutdown signal, cleaning up...");
        } catch (Exception e) {
            /* TODO: Change to the needed Exception types when done testing */
            Log.w(TAG, e);
        } finally {
            BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
            int state = (adapter != null) ? adapter.getState() : -1;
            if (state != BluetoothAdapter.STATE_ON) {
                if (DEBUG) Log.d(TAG, "BT State :" + state);
                mDeinitSignal.countDown();
            }
            // Do cleanup even if an exception occurs
            stopDisconnectTimer();
            /* In case of e.g. a RFCOMM close while connected:
             *        - Initiate a FORCED shutdown
             *        - Wait for RIL deinit to complete
             */
            if (mState == SAP_STATE.CONNECTING_CALL_ONGOING) {
                /* Most likely remote device closed rfcomm, update state */
                changeState(SAP_STATE.DISCONNECTED);
            } else if (mState != SAP_STATE.DISCONNECTED) {
                if (mState != SAP_STATE.DISCONNECTING && !mIsLocalInitDisconnect) {
                    sendDisconnectInd(SapMessage.DISC_FORCED);
                }
                if (DEBUG) {
                    Log.i(TAG, "Waiting for deinit to complete");
                }
                try {
                    mDeinitSignal.await();
                } catch (InterruptedException e) {
                    Log.e(TAG, "Interrupt received while waitinf for de-init to complete", e);
                }
            }

            if (mIntentReceiver != null) {
                mContext.unregisterReceiver(mIntentReceiver);
                mIntentReceiver = null;
            }
            stopDisconnectTimer();
            clearNotification();

            if (mHandlerThread != null) {
                try {
                    mHandlerThread.quitSafely();
                    mHandlerThread.join(HANDLER_THREAD_JOIN_TIMEOUT_MS);
                    mHandlerThread = null;
                } catch (InterruptedException e) {
                }
            }
            if (mRilBtReceiver != null) {
                mRilBtReceiver.resetSapProxy();
                mRilBtReceiver = null;
            }

            if (mRfcommIn != null) {
                try {
                    if (VERBOSE) {
                        Log.i(TAG, "Closing mRfcommIn...");
                    }
                    mRfcommIn.close();
                    mRfcommIn = null;
                } catch (IOException e) {
                }
            }

            if (mRfcommOut != null) {
                try {
                    if (VERBOSE) {
                        Log.i(TAG, "Closing mRfcommOut...");
                    }
                    mRfcommOut.close();
                    mRfcommOut = null;
                } catch (IOException e) {
                }
            }

            if (mSapServiceHandler != null) {
                Message msg = Message.obtain(mSapServiceHandler);
                msg.what = SapService.MSG_SERVERSESSION_CLOSE;
                msg.sendToTarget();
                if (DEBUG) {
                    Log.d(TAG, "MSG_SERVERSESSION_CLOSE sent out.");
                }
            }
            Log.i(TAG, "All done exiting thread...");
        }
    }


    /**
     * This function needs to determine:
     * - if the maxMsgSize is acceptable - else reply CON_STATUS_ERROR_MAX_MSG_SIZE_UNSUPPORTED
     * + new maxMsgSize if too big
     * - connect to the RIL-BT socket
     * - if a call is ongoing reply CON_STATUS_OK_ONGOING_CALL.
     * - if all ok, just respond CON_STATUS_OK.
     *
     * @param msg the incoming SapMessage
     */
    @VisibleForTesting
    void onConnectRequest(SapMessage msg) {
        SapMessage reply = new SapMessage(SapMessage.ID_CONNECT_RESP);

        if (mState == SAP_STATE.CONNECTING) {
            /* A connect request might have been rejected because of maxMessageSize negotiation, and
             * this is a new connect request. Simply forward to RIL, and stay in connecting state.
             * */
            reply = null;
            sendRilMessage(msg);
            stopDisconnectTimer();

        } else if (mState != SAP_STATE.DISCONNECTED
                && mState != SAP_STATE.CONNECTING_CALL_ONGOING) {
            reply.setConnectionStatus(SapMessage.CON_STATUS_ERROR_CONNECTION);
        } else {
            // Store the MaxMsgSize for future use
            mMaxMsgSize = msg.getMaxMsgSize();
            // All parameters OK, examine if a call is ongoing and start the RIL-BT listener thread
            if (isCallOngoing()) {
                /* If a call is ongoing we set the state, inform the SAP client and wait for a state
                 * change intent from the TelephonyManager with state IDLE. */
                reply.setConnectionStatus(SapMessage.CON_STATUS_OK_ONGOING_CALL);
            } else {
                /* no call is ongoing, initiate the connect sequence:
                 *  1) Start the SapRilReceiver thread (open the rild-bt socket)
                 *  2) Send a RIL_SIM_SAP_CONNECT request to RILD
                 *  3) Send a RIL_SIM_RESET request and a connect confirm to the SAP client */
                changeState(SAP_STATE.CONNECTING);
                if (mRilBtReceiver != null) {
                    // Notify the SapServer that we have connected to the SAP service
                    mRilBtReceiver.sendRilConnectMessage();
                    // Don't send reply yet
                    reply = null;
                } else {
                    reply = new SapMessage(SapMessage.ID_CONNECT_RESP);
                    reply.setConnectionStatus(SapMessage.CON_STATUS_ERROR_CONNECTION);
                    sendClientMessage(reply);
                }
            }
        }
        if (reply != null) {
            sendClientMessage(reply);
        }
    }

    @VisibleForTesting
    void clearPendingRilResponses(SapMessage msg) {
        if (mState == SAP_STATE.CONNECTED_BUSY) {
            msg.setClearRilQueue(true);
        }
    }

    /**
     * Send RFCOMM message to the Sap Server Handler Thread
     *
     * @param sapMsg The message to send
     */
    @VisibleForTesting
    void sendClientMessage(SapMessage sapMsg) {
        Message newMsg = mSapHandler.obtainMessage(SAP_MSG_RFC_REPLY, sapMsg);
        mSapHandler.sendMessage(newMsg);
    }

    /**
     * Send a RIL message to the SapServer message handler thread
     */
    @VisibleForTesting
    void sendRilThreadMessage(SapMessage sapMsg) {
        Message newMsg = mSapHandler.obtainMessage(SAP_MSG_RIL_REQ, sapMsg);
        mSapHandler.sendMessage(newMsg);
    }

    /**
     * Examine if a call is ongoing, by asking the telephony manager
     *
     * @return false if the phone is IDLE (can be used for SAP), true otherwise.
     */
    @VisibleForTesting
    boolean isCallOngoing() {
        TelephonyManager tManager = mContext.getSystemService(TelephonyManager.class);
        if (tManager.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
            return false;
        }
        return true;
    }

    /**
     * Change the SAP Server state.
     * We add thread protection, as we access the state from two threads.
     */
    @VisibleForTesting
    void changeState(SAP_STATE newState) {
        if (DEBUG) {
            Log.i(TAG_HANDLER, "Changing state from " + mState.name() + " to " + newState.name());
        }
        synchronized (this) {
            mState = newState;
        }
    }

    /*************************************************************************
     * SAP Server Message Handler Thread Functions
     *************************************************************************/

    /**
     * The SapServer message handler thread implements the SAP state machine.
     * - Handle all outgoing communication to the out-socket. Either replies from the RIL or direct
     * messages send from the SapServe (e.g. connect_resp).
     * - Handle all outgoing communication to the RIL-BT socket.
     * - Handle all replies from the RIL
     */
    @Override
    public boolean handleMessage(Message msg) {
        if (VERBOSE) {
            Log.i(TAG_HANDLER,
                    "Handling message (ID: " + msg.what + "): " + getMessageName(msg.what));
        }

        SapMessage sapMsg = null;

        switch (msg.what) {
            case SAP_MSG_RFC_REPLY:
                sapMsg = (SapMessage) msg.obj;
                handleRfcommReply(sapMsg);
                break;
            case SAP_MSG_RIL_CONNECT:
                /* The connection to rild-bt have been established. Store the outStream handle
                 * and send the connect request. */
                if (mTestMode != SapMessage.INVALID_VALUE) {
                    SapMessage rilTestModeReq =
                            new SapMessage(SapMessage.ID_RIL_SIM_ACCESS_TEST_REQ);
                    rilTestModeReq.setTestMode(mTestMode);
                    sendRilMessage(rilTestModeReq);
                    mTestMode = SapMessage.INVALID_VALUE;
                }
                SapMessage rilSapConnect = new SapMessage(SapMessage.ID_CONNECT_REQ);
                rilSapConnect.setMaxMsgSize(mMaxMsgSize);
                sendRilMessage(rilSapConnect);
                break;
            case SAP_MSG_RIL_REQ:
                sapMsg = (SapMessage) msg.obj;
                if (sapMsg != null) {
                    sendRilMessage(sapMsg);
                }
                break;
            case SAP_MSG_RIL_IND:
                sapMsg = (SapMessage) msg.obj;
                handleRilInd(sapMsg);
                break;
            case SAP_RIL_SOCK_CLOSED:
            /* The RIL socket was closed unexpectedly, send immediate disconnect indication
               - close RFCOMM after timeout if no response. */
                startDisconnectTimer(SapMessage.DISC_RFCOMM, DISCONNECT_TIMEOUT_RFCOMM);
                break;
            case SAP_PROXY_DEAD:
                mRilBtReceiver.notifyShutdown(); /* Only needed in case of a connection error */
                break;
            default:
                /* Message not handled */
                return false;
        }
        return true; // Message handles
    }

    /**
     * Close the in/out rfcomm streams, to trigger a shutdown of the SapServer main thread.
     * Use this after completing the deinit sequence.
     */
    @VisibleForTesting
    void shutdown() {

        if (DEBUG) {
            Log.i(TAG_HANDLER, "in Shutdown()");
        }
        try {
            if (mRfcommOut != null) {
                mRfcommOut.close();
            }
        } catch (IOException e) {
        }
        try {
            if (mRfcommIn != null) {
                mRfcommIn.close();
            }
        } catch (IOException e) {
        }
        mRfcommIn = null;
        mRfcommOut = null;
        stopDisconnectTimer();
        clearNotification();
    }

    @VisibleForTesting
    void startDisconnectTimer(int discType, int timeMs) {

        stopDisconnectTimer();
        synchronized (this) {
            Intent sapDisconnectIntent = new Intent(SapServer.SAP_DISCONNECT_ACTION);
            sapDisconnectIntent.putExtra(SAP_DISCONNECT_TYPE_EXTRA, discType);
            AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
            mPendingDiscIntent = PendingIntent.getBroadcast(mContext, discType, sapDisconnectIntent,
                    PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_IMMUTABLE);
            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
                    SystemClock.elapsedRealtime() + timeMs, mPendingDiscIntent);

            if (VERBOSE) {
                Log.d(TAG_HANDLER,
                        "Setting alarm for " + timeMs + " ms to activate disconnect type "
                                + discType);
            }
        }
    }

    @VisibleForTesting
    void stopDisconnectTimer() {
        synchronized (this) {
            if (mPendingDiscIntent != null) {
                AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class);
                alarmManager.cancel(mPendingDiscIntent);
                mPendingDiscIntent.cancel();
                if (VERBOSE) {
                    Log.d(TAG_HANDLER, "Canceling disconnect alarm");
                }
                mPendingDiscIntent = null;
            }
        }
    }

    /**
     * Here we handle the replies to the SAP client, normally forwarded directly from the RIL.
     * We do need to handle some of the messages in the SAP profile, hence we look at the messages
     * here before they go to the client
     *
     * @param sapMsg the message to send to the SAP client
     */
    @VisibleForTesting
    void handleRfcommReply(SapMessage sapMsg) {
        if (sapMsg != null) {

            if (DEBUG) {
                Log.i(TAG_HANDLER, "handleRfcommReply() handling " + SapMessage.getMsgTypeName(
                        sapMsg.getMsgType()));
            }

            switch (sapMsg.getMsgType()) {

                case SapMessage.ID_CONNECT_RESP:
                    if (mState == SAP_STATE.CONNECTING_CALL_ONGOING) {
                        /* Hold back the connect resp if a call was ongoing when the connect req
                         * was received.
                         * A response with status call-ongoing was sent, and the connect response
                         * received from the RIL when call ends must be discarded.
                         */
                        if (sapMsg.getConnectionStatus() == SapMessage.CON_STATUS_OK) {
                            // This is successful connect response from RIL/modem.
                            changeState(SAP_STATE.CONNECTED);
                        }
                        if (VERBOSE) {
                            Log.i(TAG, "Hold back the connect resp, as a call was ongoing"
                                    + " when the initial response were sent.");
                        }
                        sapMsg = null;
                    } else if (sapMsg.getConnectionStatus() == SapMessage.CON_STATUS_OK) {
                        // This is successful connect response from RIL/modem.
                        changeState(SAP_STATE.CONNECTED);
                    } else if (sapMsg.getConnectionStatus()
                            == SapMessage.CON_STATUS_OK_ONGOING_CALL) {
                        changeState(SAP_STATE.CONNECTING_CALL_ONGOING);
                    } else if (sapMsg.getConnectionStatus() != SapMessage.CON_STATUS_OK) {
                        /* Most likely the peer will try to connect again, hence we keep the
                         * connection to RIL open and stay in connecting state.
                         *
                         * Start timer to do shutdown if a new connect request is not received in
                         * time. */
                        startDisconnectTimer(SapMessage.DISC_FORCED, DISCONNECT_TIMEOUT_RFCOMM);
                    }
                    break;
                case SapMessage.ID_DISCONNECT_RESP:
                    if (mState == SAP_STATE.DISCONNECTING) {
                        /* Close the RIL-BT output Stream and signal to SapRilReceiver to close
                         * down the input stream. */
                        if (DEBUG) {
                            Log.i(TAG,
                                    "ID_DISCONNECT_RESP received in SAP_STATE." + "DISCONNECTING.");
                        }

                        /* Send the disconnect resp, and wait for the client to close the Rfcomm,
                         * but start a timeout timer, just to be sure. Use alarm, to ensure we wake
                         * the host to close the connection to minimize power consumption. */
                        SapMessage disconnectResp = new SapMessage(SapMessage.ID_DISCONNECT_RESP);
                        changeState(SAP_STATE.DISCONNECTED);
                        sapMsg = disconnectResp;
                        startDisconnectTimer(SapMessage.DISC_RFCOMM, DISCONNECT_TIMEOUT_RFCOMM);
                        mDeinitSignal.countDown(); /* Signal deinit complete */
                    } else { /* DISCONNECTED */
                        mDeinitSignal.countDown(); /* Signal deinit complete */
                        if (mIsLocalInitDisconnect) {
                            if (VERBOSE) {
                                Log.i(TAG_HANDLER, "This is a FORCED disconnect.");
                            }
                            /* We needed to force the disconnect, hence no hope for the client to
                             * close the RFCOMM connection, hence we do it here. */
                            shutdown();
                            sapMsg = null;
                        } else {
                            /* The client must disconnect the RFCOMM, but in case it does not, we
                             * need to do it.
                             * We start an alarm, and if it triggers, we must send the
                             * MSG_SERVERSESSION_CLOSE */
                            if (VERBOSE) {
                                Log.i(TAG_HANDLER, "This is a NORMAL disconnect.");
                            }
                            startDisconnectTimer(SapMessage.DISC_RFCOMM, DISCONNECT_TIMEOUT_RFCOMM);
                        }
                    }
                    break;
                case SapMessage.ID_STATUS_IND:
                    /* Some car-kits only "likes" status indication when connected, hence discard
                     * any arriving outside this state */
                    if (mState == SAP_STATE.DISCONNECTED || mState == SAP_STATE.CONNECTING
                            || mState == SAP_STATE.DISCONNECTING) {
                        sapMsg = null;
                    }
                    if (mSapServiceHandler != null && mState == SAP_STATE.CONNECTED) {
                        Message msg = Message.obtain(mSapServiceHandler);
                        msg.what = SapService.MSG_CHANGE_STATE;
                        msg.arg1 = BluetoothSap.STATE_CONNECTED;
                        msg.sendToTarget();
                        setNotification(SapMessage.DISC_GRACEFULL, 0);
                        if (DEBUG) {
                            Log.d(TAG, "MSG_CHANGE_STATE sent out.");
                        }
                    }
                    break;
                default:
                    // Nothing special, just send the message
            }
        }

        /* Update state variable based on the number of pending commands. We are only able to
         * handle one request at the time, except from disconnect, sim off and sim reset.
         * Hence if one of these are received while in busy state, we might have a crossing
         * response, hence we must stay in BUSY state if we have an ongoing RIL request. */
        if (mState == SAP_STATE.CONNECTED_BUSY) {
            if (SapMessage.getNumPendingRilMessages() == 0) {
                changeState(SAP_STATE.CONNECTED);
            }
        }

        // This is the default case - just send the message to the SAP client.
        if (sapMsg != null) {
            sendReply(sapMsg);
        }
    }

    @VisibleForTesting
    void handleRilInd(SapMessage sapMsg) {
        if (sapMsg == null) {
            return;
        }

        switch (sapMsg.getMsgType()) {
            case SapMessage.ID_RIL_UNSOL_DISCONNECT_IND: {
                if (mState != SAP_STATE.DISCONNECTED && mState != SAP_STATE.DISCONNECTING) {
                    /* we only send disconnect indication to the client if we are actually
                    connected*/
                    SapMessage reply = new SapMessage(SapMessage.ID_DISCONNECT_IND);
                    reply.setDisconnectionType(sapMsg.getDisconnectionType());
                    sendClientMessage(reply);
                } else {
                    /* TODO: This was introduced to handle disconnect indication from RIL */
                    sendDisconnectInd(sapMsg.getDisconnectionType());
                }
                break;
            }

            default:
                if (DEBUG) {
                    Log.w(TAG_HANDLER, "Unhandled message - type: " + SapMessage.getMsgTypeName(
                            sapMsg.getMsgType()));
                }
        }
    }

    /**
     * This is only to be called from the handlerThread, else use sendRilThreadMessage();
     */
    @VisibleForTesting
    void sendRilMessage(SapMessage sapMsg) {
        if (VERBOSE) {
            Log.i(TAG_HANDLER,
                    "sendRilMessage() - " + SapMessage.getMsgTypeName(sapMsg.getMsgType()));
        }
        synchronized (mRilBtReceiver.getSapProxyLock()) {
            if (!mRilBtReceiver.isProxyValid()) {
                Log.e(TAG_HANDLER,
                        "sendRiilMessage: Unable to send message to Ril; sapProxy is invalid");
                sendClientMessage(new SapMessage(SapMessage.ID_ERROR_RESP));
                return;
            }
            try {
                sapMsg.send(mRilBtReceiver);
                if (VERBOSE) {
                    Log.d(TAG_HANDLER, "sendRilMessage: sapMsg.callISapReq called successfully");
                }
            } catch (IllegalArgumentException e) {
                Log.e(TAG_HANDLER, "sendRilMessage: IllegalArgumentException", e);
                sendClientMessage(new SapMessage(SapMessage.ID_ERROR_RESP));
            } catch (RemoteException | RuntimeException e) {
                Log.e(TAG_HANDLER, "sendRilMessage: Unable to send message to RIL: " + e);
                sendClientMessage(new SapMessage(SapMessage.ID_ERROR_RESP));
                mRilBtReceiver.notifyShutdown(); /* Only needed in case of a connection error */
                mRilBtReceiver.resetSapProxy();
            }
        }

    }

    /**
     * Only call this from the sapHandler thread.
     */
    @VisibleForTesting
    void sendReply(SapMessage msg) {
        if (VERBOSE) {
            Log.i(TAG_HANDLER,
                    "sendReply() RFCOMM - " + SapMessage.getMsgTypeName(msg.getMsgType()));
        }
        if (mRfcommOut != null) { // Needed to handle brutal shutdown from car-kit and out of range
            try {
                msg.write(mRfcommOut);
                mRfcommOut.flush();
            } catch (IOException e) {
                Log.w(TAG_HANDLER, e);
                /* As we cannot write to the rfcomm channel we are disconnected.
                   Shutdown and prepare for a new connect. */
            }
        }
    }

    @VisibleForTesting
    static String getMessageName(int messageId) {
        switch (messageId) {
            case SAP_MSG_RFC_REPLY:
                return "SAP_MSG_REPLY";
            case SAP_MSG_RIL_CONNECT:
                return "SAP_MSG_RIL_CONNECT";
            case SAP_MSG_RIL_REQ:
                return "SAP_MSG_RIL_REQ";
            case SAP_MSG_RIL_IND:
                return "SAP_MSG_RIL_IND";
            default:
                return "Unknown message ID";
        }
    }

}

