| 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.hardware.radio.V1_0.ISap; |
| 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 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; |
| |
| private enum SAP_STATE { |
| DISCONNECTED, CONNECTING, CONNECTING_CALL_ONGOING, CONNECTED, CONNECTED_BUSY, DISCONNECTING; |
| } |
| |
| private 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 */ |
| private SapRilReceiver mRilBtReceiver = null; |
| /* The message handler members */ |
| private 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 */ |
| private 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; |
| private 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 |
| |
| /* 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 */ |
| private 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.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED); |
| filter.addAction(SAP_DISCONNECT_ACTION); |
| mIntentReceiver = new SapServerBroadcastReceiver(); |
| mContext.registerReceiver(mIntentReceiver, filter); |
| } |
| |
| /** |
| * This handles the response from RIL. |
| */ |
| private BroadcastReceiver mIntentReceiver; |
| |
| private 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) { |
| mTestMode = testMode; |
| } |
| } |
| |
| private 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); |
| |
| } 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 = |
| (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); |
| NotificationChannel notificationChannel = new NotificationChannel(SAP_NOTIFICATION_CHANNEL, |
| mContext.getString(R.string.bluetooth_sap_notif_title), |
| NotificationManager.IMPORTANCE_HIGH); |
| notificationManager.createNotificationChannel(notificationChannel); |
| 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 = |
| new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true) |
| .addAction(android.R.drawable.stat_sys_data_bluetooth, button, |
| pIntentDisconnect) |
| .setContentTitle(title) |
| .setTicker(ticker) |
| .setContentText(text) |
| .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) |
| .setAutoCancel(false) |
| .setPriority(Notification.PRIORITY_MAX) |
| .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 = |
| new Notification.Builder(mContext, SAP_NOTIFICATION_CHANNEL).setOngoing(true) |
| .addAction(android.R.drawable.stat_sys_data_bluetooth, |
| mContext.getString( |
| R.string.bluetooth_sap_notif_disconnect_button), |
| pIntentDisconnect) |
| .addAction(android.R.drawable.stat_sys_data_bluetooth, |
| mContext.getString( |
| R.string.bluetooth_sap_notif_force_disconnect_button), |
| pIntentForceDisconnect) |
| .setContentTitle(title) |
| .setTicker(ticker) |
| .setContentText(text) |
| .setSmallIcon(android.R.drawable.stat_sys_data_bluetooth) |
| .setAutoCancel(false) |
| .setPriority(Notification.PRIORITY_MAX) |
| .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 = |
| (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); |
| 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); |
| |
| mRilBtReceiver = new SapRilReceiver(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.quit(); |
| mHandlerThread.join(); |
| 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 |
| */ |
| private 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); |
| } |
| } |
| |
| private 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 |
| */ |
| private 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 |
| * @param sapMsg |
| */ |
| private 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. |
| */ |
| private boolean isCallOngoing() { |
| TelephonyManager tManager = |
| (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); |
| 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. |
| * @param newState |
| */ |
| private 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: |
| if ((long) msg.obj == mRilBtReceiver.mSapProxyCookie.get()) { |
| mRilBtReceiver.notifyShutdown(); /* Only needed in case of a connection error */ |
| mRilBtReceiver.resetSapProxy(); |
| |
| // todo: rild should be back up since message was sent with a delay. this is |
| // a hack. |
| mRilBtReceiver.getSapProxy(); |
| } |
| 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. |
| */ |
| private 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(); |
| } |
| |
| private 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 = |
| (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); |
| mPendingDiscIntent = PendingIntent.getBroadcast(mContext, discType, sapDisconnectIntent, |
| PendingIntent.FLAG_CANCEL_CURRENT); |
| 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); |
| } |
| } |
| } |
| |
| private void stopDisconnectTimer() { |
| synchronized (this) { |
| if (mPendingDiscIntent != null) { |
| AlarmManager alarmManager = |
| (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); |
| 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 |
| */ |
| private 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); |
| } |
| } |
| |
| private 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(); |
| * @param sapMsg |
| */ |
| private void sendRilMessage(SapMessage sapMsg) { |
| if (VERBOSE) { |
| Log.i(TAG_HANDLER, |
| "sendRilMessage() - " + SapMessage.getMsgTypeName(sapMsg.getMsgType())); |
| } |
| |
| Log.d(TAG_HANDLER, "sendRilMessage: calling getSapProxy"); |
| synchronized (mRilBtReceiver.getSapProxyLock()) { |
| ISap sapProxy = mRilBtReceiver.getSapProxy(); |
| if (sapProxy == null) { |
| Log.e(TAG_HANDLER, |
| "sendRilMessage: Unable to send message to RIL; sapProxy is null"); |
| sendClientMessage(new SapMessage(SapMessage.ID_ERROR_RESP)); |
| return; |
| } |
| |
| try { |
| sapMsg.send(sapProxy); |
| 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. |
| */ |
| private 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. */ |
| } |
| } |
| } |
| |
| private 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"; |
| } |
| } |
| |
| } |