/*
* Copyright (C) 2011 Stanford University MobiSocial Lab
* http://mobisocial.stanford.edu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mobisocial.nfc.addon;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Random;
import java.util.UUID;
import mobisocial.nfc.ConnectionHandover;
import mobisocial.nfc.NdefFactory;
import mobisocial.nfc.Nfc;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.util.Log;
import mobisocial.nfc.ConnectionHandoverManager;
/**
* <p>Allows two devices to establish a Bluetooth connection after exchanging an NFC
* Connection Handover Request. The socket is returned via callback.
*
* <p>A simple example for establishing a Bluetooth connection when both phones
* are in the same activity:
*
* <pre class="prettyprint">
* MyActivity extends Activity {
* Nfc mNfc;
*
* BluetoothConnector.OnConnectedListener mBtListener =
* new BluetoothConnector.OnConnectedListener() {
*
* public void onConnectionEstablished(BluetoothSocket socket,
* boolean isServer) {
* Log.d(TAG, "Connected over Bluetooth as " +
* (isServer ? "server" : "client"));
* }
* }
*
* public void onCreate(Bundle bundle) {
* super.onCreate(bundle);
* mNfc = new Nfc(this);
* BluetoothConnector.prepare(mNfc, mBtListener);
* }
*
* public void onResume() {
* super.onResume();
* mNfc.onResume(this);
* }
*
* public void onPause() {
* super.onPause();
* mNfc.onPause();
* }
*
* public void onNewInent(Intent intent) {
* if (mNfc.onNewIntent(this, intent)) return;
* }
* }
* </pre>
*
* <p>A more complex example, which supports:
* <ul>
* <li>Pairing when both phones are in the same activity
* <li>Pairing when only one phone is in the activity
* <li>Providing a download link if your application is not yet installed.
* </ul>
*
* <p>You should also ensure that Bluetooth and Nfc are enabled on the device.
*
* <pre class="prettyprint">
* public class MyActivity extends Activity {
* private Nfc mNfc;
* private Long mLastPausedMillis = 0L;
*
* public void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
* setContentView(R.layout.main);
* mNfc = new Nfc(this);
*
* // If this activity was launched from an NFC interaction, start the
* // Bluetooth connection process.
* if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
* BluetoothConnector.join(mNfc, mBluetoothConnected, getNdefMessages(getIntent())[0]);
* } else {
* // If both phones are running this activity, or to allow remote
* // device to join from home screen.
* BluetoothConnector.prepare(mNfc, mBluetoothConnected, getAppReference());
* }
* }
*
* protected void onResume() {
* super.onResume();
* mNfc.onResume(this);
* }
*
* protected void onPause() {
* super.onPause();
* mLastPausedMillis = System.currentTimeMillis();
* mNfc.onPause(this);
* }
*
* protected void onNewIntent(Intent intent) {
* // Check for "warm boot" if the activity uses singleInstance launch mode:
* if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
* Long ms = System.currentTimeMillis() - mLastPausedMillis;
* if (ms > 150) {
* BluetoothConnector.join(mNfc, mBluetoothConnected, getNdefMessages(intent)[0]);
* return;
* }
* }
* if (mNfc.onNewIntent(this, intent)) {
* return;
* }
* }
*
* public NdefRecord[] getAppReference() {
* byte[] urlBytes = "http://example.com/funapp".getBytes();
* NdefRecord ref = new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, NdefRecord.RTD_URI, new byte[]{}, urlBytes);
* return new NdefRecord[] { ref };
* }
*
* OnConnectedListener mBluetoothConnected = new OnConnectedListener() {
* public void onConnectionEstablished(BluetoothSocket socket, boolean isServer) {
* toast("connected! server: " + isServer);
* }
* };
*
* private void toast(final String text) {
* runOnUiThread(new Runnable() {
* public void run() {
* Toast.makeText(MyActivity.this, text, Toast.LENGTH_SHORT).show();
* }
* });
* }
*
* private NdefMessage[] getNdefMessages(Intent intent) {
* if (!intent.hasExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
* return null;
* }
* Parcelable[] msgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
* NdefMessage[] ndef = new NdefMessage[msgs.length];
* for (int i = 0; i < msgs.length; i++) {
* ndef[i] = (NdefMessage) msgs[i];
* }
* return ndef;
* }
* }
* </pre>
*
* You will also need to add an intent filter to your application's manifest:
* <pre class="prettyprint">
* <activity android:name=".MyActivity">
* <intent-filter>
* <action android:name="android.nfc.action.NDEF_DISCOVERED" />
* <category android:name="android.intent.category.DEFAULT" />
* <data android:scheme="http"
* android:host="example.com"
* android:path="/funapp" />
* </intent-filter>
* </activity>
* </pre>
*
* For devices supporting SDK 14 and above, the handover record also includes
* an Android Application Record, allowing your application to be discovered in
* the market if it is not yet installed. Otherwise, the uri provided by
* getAppReference() should direct the user to a web page relevant to your
* application.
*/
public abstract class BluetoothConnector {
private static final String SERVICE_NAME = "NfcBtHandover";
private static final String BT_SOCKET_SCHEMA = "btsocket://";
private static final String TAG = "btconnect";
private static final boolean DBG = false;
/**
* Configures the {@link mobisocial.nfc.Nfc} interface to set up a Bluetooth
* socket with another device. The method both sets the foreground ndef
* messages and registers an {@link mobisocial.nfc.NdefHandler} to look for
* incoming pairing requests.
*
* <p>When this method is called, a Bluetooth server socket is created,
* and the socket is closed after a successful connection. You must call
* prepare() again to reinitiate the server socket.
*
* @return The server socket listening for peers.
*/
public static BluetoothServerSocket prepare(Nfc nfc, OnConnectedListener conn) {
BluetoothConnecting btConnecting = new BluetoothConnecting(conn);
nfc.getConnectionHandoverManager().addConnectionHandover(btConnecting);
nfc.share(btConnecting.getHandoverRequestMessage(nfc.getContext()));
return btConnecting.mAcceptThread.mmServerSocket;
}
/**
* Configures the {@link mobisocial.nfc.Nfc} interface to set up a Bluetooth
* socket with another device. The method both sets the foreground ndef
* messages and registers an {@link mobisocial.nfc.NdefHandler} to look for
* incoming pairing requests.
*
* <p>When this method is called, a Bluetooth server socket is created,
* and the socket is closed after a successful connection. You must call
* prepare() again to reinitiate the server socket.
*
* @return The server socket listening for peers.
*/
public static BluetoothServerSocket prepare(Nfc nfc, OnConnectedListener conn, NdefRecord[] ndef) {
BluetoothConnecting btConnecting = new BluetoothConnecting(conn);
NdefMessage handoverRequest = btConnecting.getHandoverRequestMessage(nfc.getContext());
NdefRecord[] combinedRecords = new NdefRecord[ndef.length + handoverRequest.getRecords().length];
int i = 0;
for (NdefRecord r : ndef) {
combinedRecords[i++] = r;
}
for (NdefRecord r : handoverRequest.getRecords()) {
combinedRecords[i++] = r;
}
NdefMessage outbound = new NdefMessage(combinedRecords);
nfc.getConnectionHandoverManager().addConnectionHandover(btConnecting);
nfc.share(outbound);
return btConnecting.mAcceptThread.mmServerSocket;
}
/**
* Extracts the Bluetooth socket information from an ndef message and
* connects as a client.
*/
public static void join(Nfc nfc, OnConnectedListener conn, NdefMessage ndef) {
BluetoothConnecting btConnecting = new BluetoothConnecting(conn, true, UUID.randomUUID());
ConnectionHandoverManager manager = new ConnectionHandoverManager();
manager.addConnectionHandover(btConnecting);
manager.doHandover(ndef);
}
private static class BluetoothConnecting implements ConnectionHandover {
private final AcceptThread mAcceptThread;
private final byte[] mCollisionResolution;
private final OnConnectedListener mmBtConnected;
private final BluetoothAdapter mBluetoothAdapter;
private final UUID mServiceUuid;
private final int mChannel;
private final boolean mAlwaysClient;
private boolean mConnectionStarted;
public BluetoothConnecting(OnConnectedListener onBtConnected, boolean alwaysClient,
UUID serviceUuid) {
mAlwaysClient = alwaysClient;
mmBtConnected = onBtConnected;
mServiceUuid = serviceUuid;
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
throw new IllegalStateException("No Bluetooth adapter found.");
}
Random random = new Random();
mCollisionResolution = new byte[2];
random.nextBytes(mCollisionResolution);
mAcceptThread = new AcceptThread();
mChannel = mAcceptThread.getListeningPort();
mAcceptThread.start();
}
public BluetoothConnecting(OnConnectedListener onBtConnected) {
this(onBtConnected, false, UUID.randomUUID());
}
private NdefMessage getHandoverRequestMessage(Context context) {
NdefRecord[] records = new NdefRecord[4];
/* Handover Request */
byte[] version = new byte[] {
(0x1 << 4) | (0x2)
};
records[0] = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST,
new byte[0], version);
/* Collision Resolution */
records[1] = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, new byte[] {
0x63, 0x72
}, new byte[0], mCollisionResolution);
/* Handover record */
StringBuilder btRequest = new StringBuilder(BT_SOCKET_SCHEMA)
.append(mBluetoothAdapter.getAddress())
.append("/")
.append(mServiceUuid);
if (mChannel != -1) {
btRequest.append("?channel=" + mChannel);
}
records[2] = new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, NdefRecord.RTD_URI,
new byte[0], btRequest.toString().getBytes());
records[3] = NdefFactory.createApplicationRecord(context.getPackageName());
NdefMessage ndef = new NdefMessage(records);
return ndef;
}
@Override
public void doConnectionHandover(NdefMessage handoverRequest, int handover, int record)
throws IOException {
byte[] remoteCollision = handoverRequest.getRecords()[handover + 1].getPayload();
if (remoteCollision[0] == mCollisionResolution[0]
&& remoteCollision[1] == mCollisionResolution[1]) {
return; // They'll have to try again.
}
boolean amServer = (remoteCollision[0] < mCollisionResolution[0] ||
(remoteCollision[0] == mCollisionResolution[0] && remoteCollision[1] < mCollisionResolution[1]));
if (mAlwaysClient) {
amServer = false;
}
if (!mConnectionStarted) {
synchronized(BluetoothConnecting.this) {
if (!mConnectionStarted) {
mConnectionStarted = true;
mmBtConnected.beforeConnect(amServer);
}
}
}
if (!amServer) {
// Not waiting for a connection:
mAcceptThread.cancel();
Uri uri = Uri.parse(new String(handoverRequest.getRecords()[record].getPayload()));
UUID serviceUuid = UUID.fromString(uri.getPath().substring(1));
int channel = -1;
String channelStr = uri.getQueryParameter("channel");
if (null != channelStr) {
channel = Integer.parseInt(channelStr);
}
BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(uri.getAuthority());
new ConnectThread(remoteDevice, serviceUuid, channel).start();
}
}
@Override
public boolean supportsRequest(NdefRecord handoverRequest) {
if (handoverRequest.getTnf() != NdefRecord.TNF_ABSOLUTE_URI
|| !Arrays.equals(handoverRequest.getType(), NdefRecord.RTD_URI)) {
return false;
}
String uriString = new String(handoverRequest.getPayload());
if (uriString.startsWith(BT_SOCKET_SCHEMA)) {
return true;
}
return false;
}
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device, UUID uuid, int channel) {
mmDevice = device;
BluetoothSocket tmp = null;
try {
tmp = createBluetoothSocket(mmDevice, uuid, channel);
} catch (IOException e) {
Log.e(TAG, "create() failed", e);
}
mmSocket = tmp;
}
public void run() {
setName("ConnectThread");
try {
mmSocket.connect();
} catch (IOException e) {
Log.e(TAG, "failed to connect to bluetooth socket", e);
try {
mmSocket.close();
} catch (IOException e2) {
Log.e(TAG, "unable to close() socket during connection failure", e2);
}
return;
}
mmBtConnected.onConnectionEstablished(mmSocket, false);
}
}
private class AcceptThread extends Thread {
// The local server socket
private final BluetoothServerSocket mmServerSocket;
private final int mmListeningPort;
private AcceptThread() {
BluetoothServerSocket tmp = null;
// Create a new listening server socket
int listeningPort = -1;
try {
tmp = getBluetoothServerSocket();
listeningPort = getBluetoothListeningPort(tmp);
} catch (IOException e) {
Log.e(TAG, "listen() failed", e);
}
mmListeningPort = listeningPort;
mmServerSocket = tmp;
}
public void run() {
setName("AcceptThread");
BluetoothSocket socket = null;
// Wait for one connection.
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = mmServerSocket.accept();
if (!mConnectionStarted) {
synchronized(BluetoothConnecting.this) {
if (!mConnectionStarted) {
mConnectionStarted = true;
mmBtConnected.beforeConnect(true);
}
}
}
} catch (IOException e) {
if (DBG) Log.e(TAG, "accept() failed", e);
return;
}
if (socket == null) {
return;
}
try {
mmServerSocket.close();
} catch (IOException e) {
}
mmBtConnected.onConnectionEstablished(socket, true);
}
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) {
}
}
public int getListeningPort() {
return mmListeningPort;
}
}
private BluetoothServerSocket getBluetoothServerSocket() throws IOException {
BluetoothServerSocket tmp;
if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD_MR1) {
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(SERVICE_NAME,
mServiceUuid);
if (DBG) Log.d(TAG, "Using secure bluetooth server socket");
} else {
try {
// compatibility with pre SDK 10 devices
Method listener = mBluetoothAdapter.getClass().getMethod(
"listenUsingInsecureRfcommWithServiceRecord", String.class, UUID.class);
tmp = (BluetoothServerSocket) listener.invoke(mBluetoothAdapter, SERVICE_NAME, mServiceUuid);
if (DBG) Log.d(TAG, "Using insecure bluetooth server socket");
} catch (NoSuchMethodException e) {
Log.wtf(TAG, "listenUsingInsecureRfcommWithServiceRecord not found");
throw new IOException(e);
} catch (InvocationTargetException e) {
Log.wtf(TAG,
"listenUsingInsecureRfcommWithServiceRecord not available on mBtAdapter");
throw new IOException(e);
} catch (IllegalAccessException e) {
Log.wtf(TAG,
"listenUsingInsecureRfcommWithServiceRecord not available on mBtAdapter");
throw new IOException(e);
}
}
return tmp;
}
private BluetoothSocket createBluetoothSocket(BluetoothDevice device, UUID uuid, int channel)
throws IOException {
BluetoothSocket tmp;
if (channel != -1) {
try {
if (DBG) Log.d(TAG, "trying to connect to channel " + channel);
Method listener = device.getClass().getMethod("createInsecureRfcommSocket", int.class);
return (BluetoothSocket) listener.invoke(device, channel);
} catch (Exception e) {
if (DBG) Log.w(TAG, "Could not connect to channel.", e);
}
}
if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD_MR1) {
tmp = device.createRfcommSocketToServiceRecord(uuid);
if (DBG) Log.d(TAG, "Using secure bluetooth socket");
} else {
try {
// compatibility with pre SDK 10 devices
Method listener = device.getClass().getMethod(
"createInsecureRfcommSocketToServiceRecord", UUID.class);
tmp = (BluetoothSocket) listener.invoke(device, uuid);
if (DBG) Log.d(TAG, "Using insecure bluetooth socket");
} catch (NoSuchMethodException e) {
Log.wtf(TAG, "createInsecureRfcommSocketToServiceRecord not found");
throw new IOException(e);
} catch (InvocationTargetException e) {
Log.wtf(TAG,
"createInsecureRfcommSocketToServiceRecord not available on mBtAdapter");
throw new IOException(e);
} catch (IllegalAccessException e) {
Log.wtf(TAG,
"createInsecureRfcommSocketToServiceRecord not available on mBtAdapter");
throw new IOException(e);
}
}
return tmp;
}
private static int getBluetoothListeningPort(BluetoothServerSocket serverSocket) {
try {
Field socketField = BluetoothServerSocket.class.getDeclaredField("mSocket");
socketField.setAccessible(true);
BluetoothSocket socket = (BluetoothSocket)socketField.get(serverSocket);
Field portField = BluetoothSocket.class.getDeclaredField("mPort");
portField.setAccessible(true);
int port = (Integer)portField.get(socket);
return port;
} catch (Exception e) {
Log.d(TAG, "Error getting port from socket", e);
return -1;
}
}
}
/**
* A callback used when a Bluetooth connection has been established.
*/
public interface OnConnectedListener {
/**
* The method called when a Bluetooth connection has been established.
*
* @param socket The connected Bluetooth socket.
* @param isServer True if this connection is the "host" of this
* connection. Useful in establishing an asymmetric
* relationship between otherwise symmetric devices.
*/
public void onConnectionEstablished(BluetoothSocket socket, boolean isServer);
/**
* Called before an attempt to set up a Bluetooth connection.
* @param isServer True if this connection is the "host" of this
* connection. Useful in establishing an asymmetric
* relationship between otherwise symmetric devices.
*/
public void beforeConnect(boolean isServer);
}
}