blob: 69f683f3906d6fa6d778f80f4deee429ef928998 [file] [log] [blame]
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";
}
}
}