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.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 | 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 = (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); 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 = (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.quitSafely(); 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); // TODO(b/171825892) Please replace FLAG_MUTABLE_UNAUDITED below // with either FLAG_IMMUTABLE (recommended) or FLAG_MUTABLE. 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); } } } 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"; } } }