├── src └── main │ └── java │ ├── mobisocial │ ├── ndefexchange │ │ ├── NdefExchangeManager.java │ │ ├── NdefExchangeContract.java │ │ ├── PendingNdefExchange.java │ │ ├── NdefBluetoothPushHandover.java │ │ ├── NdefTcpPushHandover.java │ │ └── NdefExchangeThread.java │ ├── nfc │ │ ├── PrioritizedHandler.java │ │ ├── NdefHandler.java │ │ ├── ConnectionHandover.java │ │ ├── NfcWrapper.java │ │ ├── ConnectionHandoverManager.java │ │ ├── NdefFactory.java │ │ ├── Nfc.java │ │ └── addon │ │ │ └── BluetoothConnector.java │ └── comm │ │ ├── DuplexSocket.java │ │ ├── TcpDuplexSocket.java │ │ ├── StreamDuplexSocket.java │ │ └── BluetoothDuplexSocket.java │ └── android │ └── nfc │ ├── FormatException.java │ ├── NdefMessage.java │ └── NdefRecord.java ├── README.txt └── pom.xml /src/main/java/mobisocial/ndefexchange/NdefExchangeManager.java: -------------------------------------------------------------------------------- 1 | package mobisocial.ndefexchange; 2 | 3 | import mobisocial.nfc.ConnectionHandoverManager; 4 | 5 | public class NdefExchangeManager extends ConnectionHandoverManager { 6 | public NdefExchangeManager(NdefExchangeContract ndefExchange) { 7 | addConnectionHandover(new NdefBluetoothPushHandover(ndefExchange)); 8 | addConnectionHandover(new NdefTcpPushHandover(ndefExchange)); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | EasyNFC is a library for NFC interactions on Android. Features: 2 | * Graceful degredation on platforms lacking NFC APIs 3 | * Suport "virtual NFC", allowing two devices to communicate via NDEF exchange if only one device has an active NFC radio 4 | * Helps developers set up long-lasting Bluetooth connections across devices: 5 | http://mobisocial.github.com/EasyNFC/apidocs/reference/mobisocial/nfc/addon/BluetoothConnector.html 6 | 7 | More documentation is available at: 8 | http://mobisocial.github.com/EasyNFC/apidocs/reference/mobisocial/nfc/Nfc.html 9 | -------------------------------------------------------------------------------- /src/main/java/android/nfc/FormatException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010, The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package android.nfc; 18 | 19 | public class FormatException extends Exception { 20 | public FormatException() { 21 | super(); 22 | } 23 | 24 | public FormatException(String message) { 25 | super(message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/mobisocial/ndefexchange/NdefExchangeContract.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.ndefexchange; 19 | 20 | import mobisocial.nfc.NdefHandler; 21 | import android.nfc.NdefMessage; 22 | 23 | public interface NdefExchangeContract extends NdefHandler { 24 | public NdefMessage getForegroundNdefMessage(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/PrioritizedHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.nfc; 19 | 20 | /** 21 | * Specifies the priority used to determine the order in which 22 | * handlers are executed. 23 | */ 24 | public interface PrioritizedHandler { 25 | public static final int DEFAULT_PRIORITY = 50; 26 | public abstract int getPriority(); 27 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/comm/DuplexSocket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.comm; 19 | 20 | import java.io.Closeable; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.io.OutputStream; 24 | 25 | /** 26 | * A wrapper for the standard Socket implementation. 27 | * 28 | */ 29 | public interface DuplexSocket extends Closeable { 30 | public InputStream getInputStream() throws IOException; 31 | public OutputStream getOutputStream() throws IOException; 32 | public void connect() throws IOException; 33 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/NdefHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.nfc; 19 | 20 | import android.nfc.NdefMessage; 21 | 22 | /** 23 | * A callback issued when an Nfc tag is read. 24 | */ 25 | public interface NdefHandler { 26 | public static final int NDEF_PROPAGATE = 0; 27 | public static final int NDEF_CONSUME = 1; 28 | 29 | /** 30 | * Callback issued after an NFC tag is read or an NDEF message is received 31 | * from a remote device. This method is executed off the main thread, so be 32 | * careful when updating UI elements as a result of this callback. 33 | * 34 | * @return {@link #NDEF_CONSUME} to indicate this handler has consumed the 35 | * given message, or {@link #NDEF_PROPAGATE} to pass on to the next 36 | * handler. 37 | */ 38 | public abstract int handleNdef(NdefMessage[] ndefMessages); 39 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/comm/TcpDuplexSocket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.comm; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | import java.net.Socket; 24 | 25 | public class TcpDuplexSocket implements DuplexSocket { 26 | final Socket mSocket; 27 | 28 | public TcpDuplexSocket(String host, int port) throws IOException { 29 | mSocket = new Socket(host, port); 30 | } 31 | 32 | //@Override 33 | public void connect() throws IOException { 34 | 35 | } 36 | 37 | //@Override 38 | public InputStream getInputStream() throws IOException { 39 | return mSocket.getInputStream(); 40 | } 41 | 42 | //@Override 43 | public OutputStream getOutputStream() throws IOException { 44 | return mSocket.getOutputStream(); 45 | } 46 | 47 | //@Override 48 | public void close() throws IOException { 49 | mSocket.close(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/ConnectionHandover.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.nfc; 19 | 20 | import java.io.IOException; 21 | 22 | 23 | import android.nfc.NdefMessage; 24 | import android.nfc.NdefRecord; 25 | 26 | /** 27 | * An interface for classes supporting communication established using 28 | * Nfc but with an out-of-band data transfer. 29 | */ 30 | public interface ConnectionHandover { 31 | /** 32 | * Issues a connection handover of the given type. 33 | * @param handoverRequest The connection handover request message. 34 | * @param handoverRecordNumber The index of th first record in the connection handover. 35 | * @param recordNumber The index of the handover record entry being attempted. 36 | * @throws IOException 37 | */ 38 | public void doConnectionHandover(NdefMessage handoverRequest, int handoverRecordNumber, 39 | int recordNumber) throws IOException; 40 | public boolean supportsRequest(NdefRecord record); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/mobisocial/comm/StreamDuplexSocket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.comm; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | 24 | 25 | public class StreamDuplexSocket implements DuplexSocket { 26 | final InputStream mInputStream; 27 | final OutputStream mOutputStream; 28 | 29 | public StreamDuplexSocket(InputStream in, OutputStream out) throws IOException { 30 | mInputStream = in; 31 | mOutputStream = out; 32 | } 33 | 34 | @Override 35 | public void connect() throws IOException { 36 | 37 | } 38 | 39 | @Override 40 | public InputStream getInputStream() throws IOException { 41 | return mInputStream; 42 | } 43 | 44 | @Override 45 | public OutputStream getOutputStream() throws IOException { 46 | return mOutputStream; 47 | } 48 | 49 | @Override 50 | public void close() throws IOException { 51 | mInputStream.close(); 52 | mOutputStream.close(); 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/ndefexchange/PendingNdefExchange.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.ndefexchange; 19 | 20 | import mobisocial.nfc.NdefHandler; 21 | import android.nfc.NdefMessage; 22 | 23 | /** 24 | * An Ndef Exchange connection handover that is ready to be executed. 25 | * 26 | */ 27 | public class PendingNdefExchange { 28 | private final NdefMessage mHandover; 29 | private final NdefHandler mNdefHandler; 30 | 31 | public PendingNdefExchange(NdefMessage handover, final NdefHandler ndefHandler) { 32 | mHandover = handover; 33 | mNdefHandler = ndefHandler; 34 | } 35 | 36 | public void exchangeNdef(final NdefMessage ndef) { 37 | NdefExchangeContract ndefExchange = new NdefExchangeContract() { 38 | @Override 39 | public int handleNdef(NdefMessage[] ndef) { 40 | mNdefHandler.handleNdef(ndef); 41 | return NDEF_CONSUME; 42 | } 43 | 44 | @Override 45 | public NdefMessage getForegroundNdefMessage() { 46 | return ndef; 47 | } 48 | }; 49 | 50 | new NdefExchangeManager(ndefExchange).doHandover(mHandover); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/NfcWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.nfc; 19 | 20 | import android.content.Context; 21 | import android.nfc.NfcAdapter; 22 | import android.os.Build; 23 | 24 | /** 25 | * Wraps the NfcAdapter class for use on platforms not supporting 26 | * native NFC interactions. 27 | */ 28 | abstract class NfcWrapper { 29 | public static int SDK_NDEF_EXCHANGE = 10; 30 | public static int SDK_NDEF_DEFINED = 9; 31 | 32 | public static NfcWrapper getInstance() { 33 | if (Build.VERSION.SDK_INT >= SDK_NDEF_EXCHANGE) { 34 | return NfcWrapper233.LazyHolder.sInstance; 35 | } else { 36 | return null; 37 | } 38 | } 39 | 40 | public abstract NfcAdapter getAdapter(Context c); 41 | 42 | 43 | private static class NfcWrapper233 extends NfcWrapper { 44 | private static class LazyHolder { 45 | private static final NfcWrapper sInstance = new NfcWrapper233(); 46 | } 47 | 48 | @Override 49 | public NfcAdapter getAdapter(Context c) { 50 | return NfcAdapter.getDefaultAdapter(c); 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/comm/BluetoothDuplexSocket.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.comm; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | import java.util.UUID; 24 | 25 | import android.bluetooth.BluetoothAdapter; 26 | import android.bluetooth.BluetoothDevice; 27 | import android.bluetooth.BluetoothSocket; 28 | 29 | public class BluetoothDuplexSocket implements DuplexSocket { 30 | final String mmMac; 31 | final UUID mmServiceUuid; 32 | final BluetoothAdapter mmBluetoothAdapter; 33 | BluetoothSocket mmSocket; 34 | 35 | public BluetoothDuplexSocket(BluetoothAdapter adapter, String mac, UUID serviceUuid) throws IOException { 36 | mmBluetoothAdapter = adapter; 37 | mmMac = mac; 38 | mmServiceUuid = serviceUuid; 39 | } 40 | 41 | public BluetoothDuplexSocket(BluetoothSocket socket) { 42 | mmBluetoothAdapter = null; 43 | mmMac = null; 44 | mmServiceUuid = null; 45 | 46 | mmSocket = socket; 47 | } 48 | 49 | @Override 50 | public void connect() throws IOException { 51 | if (mmSocket == null) { 52 | BluetoothDevice device = mmBluetoothAdapter.getRemoteDevice(mmMac); 53 | mmSocket = device.createInsecureRfcommSocketToServiceRecord(mmServiceUuid); 54 | mmSocket.connect(); 55 | } 56 | } 57 | 58 | @Override 59 | public InputStream getInputStream() throws IOException { 60 | return mmSocket.getInputStream(); 61 | } 62 | 63 | @Override 64 | public OutputStream getOutputStream() throws IOException { 65 | return mmSocket.getOutputStream(); 66 | } 67 | 68 | @Override 69 | public void close() throws IOException { 70 | if (mmSocket != null) { 71 | mmSocket.close(); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/ndefexchange/NdefBluetoothPushHandover.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.ndefexchange; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | import java.util.Arrays; 24 | import java.util.UUID; 25 | 26 | import mobisocial.comm.BluetoothDuplexSocket; 27 | import mobisocial.comm.DuplexSocket; 28 | import mobisocial.nfc.ConnectionHandover; 29 | import mobisocial.nfc.NdefFactory; 30 | 31 | import android.bluetooth.BluetoothAdapter; 32 | import android.bluetooth.BluetoothDevice; 33 | import android.bluetooth.BluetoothSocket; 34 | import android.net.Uri; 35 | import android.nfc.FormatException; 36 | import android.nfc.NdefMessage; 37 | import android.nfc.NdefRecord; 38 | import android.util.Log; 39 | 40 | /** 41 | *
Implements an Ndef push handover request in which a static tag 42 | * represents an Ndef reader device listening on a Bluetooth socket. 43 | *
44 | *45 | * Your application must hold the {@code android.permission.BLUETOOTH} 46 | * permission to support Bluetooth handovers. 47 | *
48 | */ 49 | public class NdefBluetoothPushHandover implements ConnectionHandover { 50 | final BluetoothAdapter mmBluetoothAdapter; 51 | private final NdefExchangeContract mNdefExchange; 52 | 53 | public NdefBluetoothPushHandover(NdefExchangeContract ndefExchange) { 54 | mmBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 55 | mNdefExchange = ndefExchange; 56 | } 57 | 58 | @Override 59 | public boolean supportsRequest(NdefRecord handoverRequest) { 60 | short tnf = handoverRequest.getTnf(); 61 | if (tnf != NdefRecord.TNF_ABSOLUTE_URI && (tnf != NdefRecord.TNF_WELL_KNOWN && 62 | !Arrays.equals(handoverRequest.getType(), NdefRecord.RTD_URI))) { 63 | return false; 64 | } 65 | Uri uri; 66 | try { 67 | uri= NdefFactory.parseUri(handoverRequest); 68 | } catch (FormatException e) { 69 | return false; 70 | } 71 | String scheme = uri.getScheme(); 72 | return (scheme != null && scheme.equals("ndef+bluetooth")); 73 | } 74 | 75 | @Override 76 | public void doConnectionHandover(NdefMessage handoverMessage, int handover, int record) throws IOException { 77 | NdefRecord handoverRequest = handoverMessage.getRecords()[record]; 78 | String uriString = new String(handoverRequest.getPayload()); 79 | Uri target = Uri.parse(uriString); 80 | 81 | String mac = target.getAuthority(); 82 | UUID uuid = UUID.fromString(target.getPath().substring(1)); 83 | DuplexSocket socket = new BluetoothDuplexSocket(mmBluetoothAdapter, mac, uuid); 84 | new NdefExchangeThread(socket, mNdefExchange).start(); 85 | } 86 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 |Implements an Ndef push handover request in which a static tag 41 | * represents an Ndef reader device listening on a TCP socket. 42 | *
43 | *44 | * Your application must hold the {@code android.permission.INTERNET} 45 | * permission to support TCP handovers. 46 | *
47 | */ 48 | public class NdefTcpPushHandover implements ConnectionHandover { 49 | private static final int DEFAULT_TCP_HANDOVER_PORT = 7924; 50 | private final NdefExchangeContract mNdefExchange; 51 | 52 | public NdefTcpPushHandover(NdefExchangeContract ndefExchange) { 53 | mNdefExchange = ndefExchange; 54 | } 55 | 56 | //@Override 57 | public boolean supportsRequest(NdefRecord handoverRequest) { 58 | short tnf = handoverRequest.getTnf(); 59 | if (tnf != NdefRecord.TNF_ABSOLUTE_URI && (tnf != NdefRecord.TNF_WELL_KNOWN && 60 | !Arrays.equals(handoverRequest.getType(), NdefRecord.RTD_URI))) { 61 | return false; 62 | } 63 | Uri uri; 64 | try { 65 | uri= NdefFactory.parseUri(handoverRequest); 66 | } catch (FormatException e) { 67 | return false; 68 | } 69 | String scheme = uri.getScheme(); 70 | return (scheme != null && scheme.equals("ndef+tcp")); 71 | } 72 | 73 | @Override 74 | public void doConnectionHandover(NdefMessage handoverMessage, int handover, int record) throws IOException { 75 | NdefRecord handoverRequest = handoverMessage.getRecords()[record]; 76 | NdefMessage outboundNdef = mNdefExchange.getForegroundNdefMessage(); 77 | if (outboundNdef == null) return; 78 | 79 | try { 80 | String uriString = NdefFactory.parseUri(handoverRequest).toString(); 81 | URI uri = URI.create(uriString); 82 | sendNdefOverTcp(uri, mNdefExchange); 83 | } catch (FormatException e) { 84 | Log.wtf("easynfc", "Bad handover request", e); 85 | } 86 | } 87 | 88 | private void sendNdefOverTcp(URI target, NdefExchangeContract ndefProxy) throws IOException { 89 | String host = target.getHost(); 90 | int port = target.getPort(); 91 | if (port == -1) { 92 | port = DEFAULT_TCP_HANDOVER_PORT; 93 | } 94 | 95 | DuplexSocket socket = new TcpDuplexSocket(host, port); 96 | new NdefExchangeThread(socket, ndefProxy).start(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/ndefexchange/NdefExchangeThread.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Stanford University MobiSocial Lab 3 | * http://mobisocial.stanford.edu 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package mobisocial.ndefexchange; 19 | 20 | import java.io.DataInputStream; 21 | import java.io.DataOutputStream; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.io.OutputStream; 25 | 26 | import mobisocial.comm.DuplexSocket; 27 | 28 | import android.nfc.NdefMessage; 29 | import android.util.Log; 30 | 31 | /** 32 | * Runs a thread during a connection handover with a remote device over a 33 | * {@see DuplexSocket}, transmitting the given Ndef message. 34 | */ 35 | public class NdefExchangeThread extends Thread { 36 | public static final String TAG = "ndefexchange"; 37 | public static final byte HANDOVER_VERSION = 0x19; 38 | private final DuplexSocket mmSocket; 39 | private final InputStream mmInStream; 40 | private final OutputStream mmOutStream; 41 | private final NdefExchangeContract mmNfcInterface; 42 | 43 | private boolean mmIsWriteDone = false; 44 | private boolean mmIsReadDone = false; 45 | 46 | public NdefExchangeThread(DuplexSocket socket, NdefExchangeContract nfcInterface) { 47 | mmNfcInterface = nfcInterface; 48 | mmSocket = socket; 49 | InputStream tmpIn = null; 50 | OutputStream tmpOut = null; 51 | 52 | try { 53 | socket.connect(); 54 | tmpIn = socket.getInputStream(); 55 | tmpOut = socket.getOutputStream(); 56 | } catch (IOException e) { 57 | Log.e(TAG, "temp sockets not created", e); 58 | } 59 | 60 | mmInStream = tmpIn; 61 | mmOutStream = tmpOut; 62 | } 63 | 64 | public void run() { 65 | try { 66 | if (mmInStream == null || mmOutStream == null) { 67 | return; 68 | } 69 | 70 | // Read on this thread, write on a new one. 71 | new SendNdefThread().start(); 72 | 73 | DataInputStream dataIn = new DataInputStream(mmInStream); 74 | byte version = (byte) dataIn.readByte(); 75 | if (version != HANDOVER_VERSION) { 76 | throw new Exception("Bad handover protocol version."); 77 | } 78 | int length = dataIn.readInt(); 79 | if (length > 0) { 80 | byte[] ndefBytes = new byte[length]; 81 | int read = 0; 82 | while (read < length) { 83 | read += dataIn.read(ndefBytes, read, (length - read)); 84 | } 85 | NdefMessage ndef = new NdefMessage(ndefBytes); 86 | mmNfcInterface.handleNdef(new NdefMessage[]{ndef}); 87 | } 88 | } catch (Exception e) { 89 | Log.e(TAG, "Failed to issue handover.", e); 90 | } finally { 91 | synchronized(NdefExchangeThread.this) { 92 | mmIsReadDone = true; 93 | if (mmIsWriteDone) { 94 | cancel(); 95 | } 96 | } 97 | } 98 | } 99 | 100 | public void cancel() { 101 | try { 102 | mmSocket.close(); 103 | } catch (IOException e) {} 104 | } 105 | 106 | 107 | private class SendNdefThread extends Thread { 108 | @Override 109 | public void run() { 110 | try { 111 | NdefMessage outbound = mmNfcInterface.getForegroundNdefMessage(); 112 | DataOutputStream dataOut = new DataOutputStream(mmOutStream); 113 | dataOut.writeByte(HANDOVER_VERSION); 114 | if (outbound != null) { 115 | byte[] ndefBytes = outbound.toByteArray(); 116 | dataOut.writeInt(ndefBytes.length); 117 | dataOut.write(ndefBytes); 118 | } else { 119 | dataOut.writeInt(0); 120 | } 121 | dataOut.flush(); 122 | } catch (IOException e) { 123 | Log.e(TAG, "Error writing to socket", e); 124 | } finally { 125 | synchronized(NdefExchangeThread.this) { 126 | mmIsWriteDone = true; 127 | if (mmIsReadDone) { 128 | cancel(); 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /src/main/java/android/nfc/NdefMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2010 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package android.nfc; 18 | 19 | import android.os.Parcel; 20 | import android.os.Parcelable; 21 | 22 | /** 23 | * Represents an NDEF (NFC Data Exchange Format) data message that contains one or more {@link 24 | * NdefRecord}s. 25 | *An NDEF message includes "records" that can contain different sets of data, such as 26 | * MIME-type media, a URI, or one of the supported RTD types (see {@link NdefRecord}). An NDEF 27 | * message always contains zero or more NDEF records.
28 | *This is an immutable data class. 29 | */ 30 | public final class NdefMessage implements Parcelable { 31 | private static final byte FLAG_MB = (byte) 0x80; 32 | private static final byte FLAG_ME = (byte) 0x40; 33 | 34 | private final NdefRecord[] mRecords; 35 | 36 | /** 37 | * Create an NDEF message from raw bytes. 38 | *
39 | * Validation is performed to make sure the Record format headers are valid,
40 | * and the ID + TYPE + PAYLOAD fields are of the correct size.
41 | * @throws FormatException
42 | */
43 | public NdefMessage(byte[] data) throws FormatException {
44 | mRecords = null; // stop compiler complaints about final field
45 | if (parseNdefMessage(data) == -1) {
46 | throw new FormatException("Error while parsing NDEF message");
47 | }
48 | }
49 |
50 | /**
51 | * Create an NDEF message from NDEF records.
52 | */
53 | public NdefMessage(NdefRecord[] records) {
54 | mRecords = new NdefRecord[records.length];
55 | System.arraycopy(records, 0, mRecords, 0, records.length);
56 | }
57 |
58 | /**
59 | * Get the NDEF records inside this NDEF message.
60 | *
61 | * @return array of zero or more NDEF records.
62 | */
63 | public NdefRecord[] getRecords() {
64 | return mRecords.clone();
65 | }
66 |
67 | /**
68 | * Returns a byte array representation of this entire NDEF message.
69 | */
70 | public byte[] toByteArray() {
71 | //TODO: allocate the byte array once, copy each record once
72 | //TODO: process MB and ME flags outside loop
73 | if ((mRecords == null) || (mRecords.length == 0))
74 | return new byte[0];
75 |
76 | byte[] msg = {};
77 |
78 | for (int i = 0; i < mRecords.length; i++) {
79 | byte[] record = mRecords[i].toByteArray();
80 | byte[] tmp = new byte[msg.length + record.length];
81 |
82 | /* Make sure the Message Begin flag is set only for the first record */
83 | if (i == 0) {
84 | record[0] |= FLAG_MB;
85 | } else {
86 | record[0] &= ~FLAG_MB;
87 | }
88 |
89 | /* Make sure the Message End flag is set only for the last record */
90 | if (i == (mRecords.length - 1)) {
91 | record[0] |= FLAG_ME;
92 | } else {
93 | record[0] &= ~FLAG_ME;
94 | }
95 |
96 | System.arraycopy(msg, 0, tmp, 0, msg.length);
97 | System.arraycopy(record, 0, tmp, msg.length, record.length);
98 |
99 | msg = tmp;
100 | }
101 |
102 | return msg;
103 | }
104 |
105 | @Override
106 | public int describeContents() {
107 | return 0;
108 | }
109 |
110 | @Override
111 | public void writeToParcel(Parcel dest, int flags) {
112 | dest.writeInt(mRecords.length);
113 | dest.writeTypedArray(mRecords, flags);
114 | }
115 |
116 | public static final Parcelable.Creator
234 | * From Android SDK, version 14. (C) 2010 The Android Open Source Project
235 | *
236 | * Creates an Android application NDEF record.
237 | *
238 | * This record indicates to other Android devices the package that should be
239 | * used to handle the rest of the NDEF message. You can embed this record
240 | * anywhere into your NDEF message to ensure that the intended package
241 | * receives the message.
242 | *
243 | * When an Android device dispatches an {@link NdefMessage} containing one
244 | * or more Android application records, the applications contained in those
245 | * records will be the preferred target for the NDEF_DISCOVERED intent, in
246 | * the order in which they appear in the {@link NdefMessage}. This dispatch
247 | * behavior was first added to Android in Ice Cream Sandwich.
248 | *
249 | * If none of the applications are installed on the device, a Market link
250 | * will be opened to the first application.
251 | *
252 | * Note that Android application records do not overrule applications that
253 | * have called {@link NfcAdapter#enableForegroundDispatch}.
254 | *
255 | * @param packageName Android package name
256 | * @return Android application NDEF record
257 | */
258 | public static NdefRecord createApplicationRecord(String packageName) {
259 | try {
260 | return new NdefRecord(NdefRecord.TNF_EXTERNAL_TYPE, RTD_ANDROID_APP, new byte[] {},
261 | packageName.getBytes("US-ASCII"));
262 | } catch (UnsupportedEncodingException e) {
263 | return null;
264 | }
265 | }
266 | }
--------------------------------------------------------------------------------
/src/main/java/android/nfc/NdefRecord.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2010 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package android.nfc;
18 |
19 | import android.os.Parcel;
20 | import android.os.Parcelable;
21 |
22 | import java.lang.UnsupportedOperationException;
23 | import java.nio.ByteBuffer;
24 |
25 | /**
26 | * Represents a logical (unchunked) NDEF (NFC Data Exchange Format) record.
27 | * An NDEF record always contains:
28 | * The underlying record
35 | * representation may be chunked across several NDEF records when the payload is
36 | * large.
37 | * This is an immutable data class.
38 | */
39 | public final class NdefRecord implements Parcelable {
40 | /**
41 | * Indicates no type, id, or payload is associated with this NDEF Record.
42 | *
43 | * Type, id and payload fields must all be empty to be a valid TNF_EMPTY
44 | * record.
45 | */
46 | public static final short TNF_EMPTY = 0x00;
47 |
48 | /**
49 | * Indicates the type field uses the RTD type name format.
50 | *
51 | * Use this TNF with RTD types such as RTD_TEXT, RTD_URI.
52 | */
53 | public static final short TNF_WELL_KNOWN = 0x01;
54 |
55 | /**
56 | * Indicates the type field contains a value that follows the media-type BNF
57 | * construct defined by RFC 2046.
58 | */
59 | public static final short TNF_MIME_MEDIA = 0x02;
60 |
61 | /**
62 | * Indicates the type field contains a value that follows the absolute-URI
63 | * BNF construct defined by RFC 3986.
64 | */
65 | public static final short TNF_ABSOLUTE_URI = 0x03;
66 |
67 | /**
68 | * Indicates the type field contains a value that follows the RTD external
69 | * name specification.
70 | *
71 | * Note this TNF should not be used with RTD_TEXT or RTD_URI constants.
72 | * Those are well known RTD constants, not external RTD constants.
73 | */
74 | public static final short TNF_EXTERNAL_TYPE = 0x04;
75 |
76 | /**
77 | * Indicates the payload type is unknown.
78 | *
79 | * This is similar to the "application/octet-stream" MIME type. The payload
80 | * type is not explicitly encoded within the NDEF Message.
81 | *
82 | * The type field must be empty to be a valid TNF_UNKNOWN record.
83 | */
84 | public static final short TNF_UNKNOWN = 0x05;
85 |
86 | /**
87 | * Indicates the payload is an intermediate or final chunk of a chunked
88 | * NDEF Record.
89 | *
90 | * The payload type is specified in the first chunk, and subsequent chunks
91 | * must use TNF_UNCHANGED with an empty type field. TNF_UNCHANGED must not
92 | * be used in any other situation.
93 | */
94 | public static final short TNF_UNCHANGED = 0x06;
95 |
96 | /**
97 | * Reserved TNF type.
98 | *
99 | * The NFC Forum NDEF Specification v1.0 suggests for NDEF parsers to treat this
100 | * value like TNF_UNKNOWN.
101 | * @hide
102 | */
103 | public static final short TNF_RESERVED = 0x07;
104 |
105 | /**
106 | * RTD Text type. For use with TNF_WELL_KNOWN.
107 | */
108 | public static final byte[] RTD_TEXT = {0x54}; // "T"
109 |
110 | /**
111 | * RTD URI type. For use with TNF_WELL_KNOWN.
112 | */
113 | public static final byte[] RTD_URI = {0x55}; // "U"
114 |
115 | /**
116 | * RTD Smart Poster type. For use with TNF_WELL_KNOWN.
117 | */
118 | public static final byte[] RTD_SMART_POSTER = {0x53, 0x70}; // "Sp"
119 |
120 | /**
121 | * RTD Alternative Carrier type. For use with TNF_WELL_KNOWN.
122 | */
123 | public static final byte[] RTD_ALTERNATIVE_CARRIER = {0x61, 0x63}; // "ac"
124 |
125 | /**
126 | * RTD Handover Carrier type. For use with TNF_WELL_KNOWN.
127 | */
128 | public static final byte[] RTD_HANDOVER_CARRIER = {0x48, 0x63}; // "Hc"
129 |
130 | /**
131 | * RTD Handover Request type. For use with TNF_WELL_KNOWN.
132 | */
133 | public static final byte[] RTD_HANDOVER_REQUEST = {0x48, 0x72}; // "Hr"
134 |
135 | /**
136 | * RTD Handover Select type. For use with TNF_WELL_KNOWN.
137 | */
138 | public static final byte[] RTD_HANDOVER_SELECT = {0x48, 0x73}; // "Hs"
139 |
140 | private static final byte FLAG_MB = (byte) 0x80;
141 | private static final byte FLAG_ME = (byte) 0x40;
142 | private static final byte FLAG_CF = (byte) 0x20;
143 | private static final byte FLAG_SR = (byte) 0x10;
144 | private static final byte FLAG_IL = (byte) 0x08;
145 |
146 | private /*final*/ byte mFlags;
147 | private /*final*/ short mTnf;
148 | private /*final*/ byte[] mType;
149 | private /*final*/ byte[] mId;
150 | private /*final*/ byte[] mPayload;
151 |
152 | /**
153 | * Construct an NDEF Record.
154 | *
155 | * Applications should not attempt to manually chunk NDEF Records - the
156 | * implementation of android.nfc will automatically chunk an NDEF Record
157 | * when necessary (and only present a single logical NDEF Record to the
158 | * application). So applications should not use TNF_UNCHANGED.
159 | *
160 | * @param tnf a 3-bit TNF constant
161 | * @param type byte array, containing zero to 255 bytes, must not be null
162 | * @param id byte array, containing zero to 255 bytes, must not be null
163 | * @param payload byte array, containing zero to (2 ** 32 - 1) bytes,
164 | * must not be null
165 | */
166 | public NdefRecord(short tnf, byte[] type, byte[] id, byte[] payload) {
167 | /* check arguments */
168 | if ((type == null) || (id == null) || (payload == null)) {
169 | throw new IllegalArgumentException("Illegal null argument");
170 | }
171 |
172 | if (tnf < 0 || tnf > 0x07) {
173 | throw new IllegalArgumentException("TNF out of range " + tnf);
174 | }
175 |
176 | /* generate flag */
177 | byte flags = FLAG_MB | FLAG_ME;
178 |
179 | /* Determine if it is a short record */
180 | if(payload.length < 0xFF) {
181 | flags |= FLAG_SR;
182 | }
183 |
184 | /* Determine if an id is present */
185 | if(id.length != 0) {
186 | flags |= FLAG_IL;
187 | }
188 |
189 | mFlags = flags;
190 | mTnf = tnf;
191 | mType = type.clone();
192 | mId = id.clone();
193 | mPayload = payload.clone();
194 | }
195 |
196 | /**
197 | * Construct an NDEF Record from raw bytes.
198 | *
199 | * Validation is performed to make sure the header is valid, and that
200 | * the id, type and payload sizes appear to be valid.
201 | *
202 | * @throws FormatException if the data is not a valid NDEF record
203 | */
204 | public NdefRecord(byte[] data) throws FormatException {
205 | /* Prevent compiler to complain about unassigned final fields */
206 | mFlags = 0;
207 | mTnf = 0;
208 | mType = null;
209 | mId = null;
210 | mPayload = null;
211 | /* Perform actual parsing */
212 | if (parseNdefRecord(data) == -1) {
213 | throw new FormatException("Error while parsing NDEF record");
214 | }
215 | }
216 |
217 | /**
218 | * Returns the 3-bit TNF.
219 | *
220 | * TNF is the top-level type.
221 | */
222 | public short getTnf() {
223 | return mTnf;
224 | }
225 |
226 | /**
227 | * Returns the variable length Type field.
228 | *
229 | * This should be used in conjunction with the TNF field to determine the
230 | * payload format.
231 | */
232 | public byte[] getType() {
233 | return mType.clone();
234 | }
235 |
236 | /**
237 | * Returns the variable length ID.
238 | */
239 | public byte[] getId() {
240 | return mId.clone();
241 | }
242 |
243 | /**
244 | * Returns the variable length payload.
245 | */
246 | public byte[] getPayload() {
247 | return mPayload.clone();
248 | }
249 |
250 | /**
251 | * Returns this entire NDEF Record as a byte array.
252 | */
253 | public byte[] toByteArray() {
254 | return generate(mFlags, mTnf, mType, mId, mPayload);
255 | }
256 |
257 | public int describeContents() {
258 | return 0;
259 | }
260 |
261 | public void writeToParcel(Parcel dest, int flags) {
262 | dest.writeInt(mTnf);
263 | dest.writeInt(mType.length);
264 | dest.writeByteArray(mType);
265 | dest.writeInt(mId.length);
266 | dest.writeByteArray(mId);
267 | dest.writeInt(mPayload.length);
268 | dest.writeByteArray(mPayload);
269 | }
270 |
271 | public static final Parcelable.Creator This class acts as an abstraction layer for Android's Nfc stack.
51 | * The goals of this project are to:
52 | *
61 | * The Nfc class must be run from a foregrounded activity. It requires
62 | * a few lifecycle events be triggered during an activity's runtime:
63 | *
95 | * Your application must hold the {@code android.permission.NFC}
96 | * permission to use this class. However, this class will degrade gracefully
97 | * on devices lacking Nfc capabilities.
98 | *
100 | * The Nfc interface can be in one of three modes: {@link #MODE_WRITE}, for writing
101 | * to a passive NFC tag, and {@link #MODE_EXCHANGE}, in which the interface can
102 | * read data from passive tags and exchange data with another active Nfc device, or
103 | * {@link #MODE_PASSTHROUGH} which disables this interface.
104 | *
29 | *
34 | *
53 | *
58 | *
65 | *
66 | * class MyActivity extends Activity {
67 | *
68 | * public void onCreate(Bundle savedInstanceState) {
69 | * super.onCreate(savedInstanceState);
70 | * mNfc = new Nfc(this);
71 | * mNfc.onCreate(this);
72 | * }
73 | *
74 | * public void onResume() {
75 | * super.onResume();
76 | * mNfc.onResume(this);
77 | * // your activity's onResume code
78 | * }
79 | *
80 | * public void onPause() {
81 | * super.onPause();
82 | * mNfc.onPause(this);
83 | * // your activity's onPause code
84 | * }
85 | *
86 | * public void onNewIntent(Intent intent) {
87 | * if (mNfc.onNewIntent(this, intent)) {
88 | * return;
89 | * }
90 | * // your activity's onNewIntent code
91 | * }
92 | * }
93 | *
94 | *
105 | *
109 | *
Allows two devices to establish a Bluetooth connection after exchanging an NFC 48 | * Connection Handover Request. The socket is returned via callback. 49 | * 50 | *
A simple example for establishing a Bluetooth connection when both phones 51 | * are in the same activity: 52 | * 53 | *
54 | * MyActivity extends Activity {
55 | * Nfc mNfc;
56 | *
57 | * BluetoothConnector.OnConnectedListener mBtListener =
58 | * new BluetoothConnector.OnConnectedListener() {
59 | *
60 | * public void onConnectionEstablished(BluetoothSocket socket,
61 | * boolean isServer) {
62 | * Log.d(TAG, "Connected over Bluetooth as " +
63 | * (isServer ? "server" : "client"));
64 | * }
65 | * }
66 | *
67 | * public void onCreate(Bundle bundle) {
68 | * super.onCreate(bundle);
69 | * mNfc = new Nfc(this);
70 | * BluetoothConnector.prepare(mNfc, mBtListener);
71 | * }
72 | *
73 | * public void onResume() {
74 | * super.onResume();
75 | * mNfc.onResume(this);
76 | * }
77 | *
78 | * public void onPause() {
79 | * super.onPause();
80 | * mNfc.onPause();
81 | * }
82 | *
83 | * public void onNewInent(Intent intent) {
84 | * if (mNfc.onNewIntent(this, intent)) return;
85 | * }
86 | * }
87 | *
88 | *
89 | * A more complex example, which supports: 90 | *
You should also ensure that Bluetooth and Nfc are enabled on the device. 97 | * 98 | *
99 | * public class MyActivity extends Activity {
100 | * private Nfc mNfc;
101 | * private Long mLastPausedMillis = 0L;
102 | *
103 | * public void onCreate(Bundle savedInstanceState) {
104 | * super.onCreate(savedInstanceState);
105 | * setContentView(R.layout.main);
106 | * mNfc = new Nfc(this);
107 | *
108 | * // If this activity was launched from an NFC interaction, start the
109 | * // Bluetooth connection process.
110 | * if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(getIntent().getAction())) {
111 | * BluetoothConnector.join(mNfc, mBluetoothConnected, getNdefMessages(getIntent())[0]);
112 | * } else {
113 | * // If both phones are running this activity, or to allow remote
114 | * // device to join from home screen.
115 | * BluetoothConnector.prepare(mNfc, mBluetoothConnected, getAppReference());
116 | * }
117 | * }
118 | *
119 | * protected void onResume() {
120 | * super.onResume();
121 | * mNfc.onResume(this);
122 | * }
123 | *
124 | * protected void onPause() {
125 | * super.onPause();
126 | * mLastPausedMillis = System.currentTimeMillis();
127 | * mNfc.onPause(this);
128 | * }
129 | *
130 | * protected void onNewIntent(Intent intent) {
131 | * // Check for "warm boot" if the activity uses singleInstance launch mode:
132 | * if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
133 | * Long ms = System.currentTimeMillis() - mLastPausedMillis;
134 | * if (ms > 150) {
135 | * BluetoothConnector.join(mNfc, mBluetoothConnected, getNdefMessages(intent)[0]);
136 | * return;
137 | * }
138 | * }
139 | * if (mNfc.onNewIntent(this, intent)) {
140 | * return;
141 | * }
142 | * }
143 | *
144 | * public NdefRecord[] getAppReference() {
145 | * byte[] urlBytes = "http://example.com/funapp".getBytes();
146 | * NdefRecord ref = new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, NdefRecord.RTD_URI, new byte[]{}, urlBytes);
147 | * return new NdefRecord[] { ref };
148 | * }
149 | *
150 | * OnConnectedListener mBluetoothConnected = new OnConnectedListener() {
151 | * public void onConnectionEstablished(BluetoothSocket socket, boolean isServer) {
152 | * toast("connected! server: " + isServer);
153 | * }
154 | * };
155 | *
156 | * private void toast(final String text) {
157 | * runOnUiThread(new Runnable() {
158 | * public void run() {
159 | * Toast.makeText(MyActivity.this, text, Toast.LENGTH_SHORT).show();
160 | * }
161 | * });
162 | * }
163 | *
164 | * private NdefMessage[] getNdefMessages(Intent intent) {
165 | * if (!intent.hasExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
166 | * return null;
167 | * }
168 | * Parcelable[] msgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
169 | * NdefMessage[] ndef = new NdefMessage[msgs.length];
170 | * for (int i = 0; i < msgs.length; i++) {
171 | * ndef[i] = (NdefMessage) msgs[i];
172 | * }
173 | * return ndef;
174 | * }
175 | * }
176 | *
177 | *
178 | * You will also need to add an intent filter to your application's manifest:
179 | * 180 | * <activity android:name=".MyActivity"> 181 | * <intent-filter> 182 | * <action android:name="android.nfc.action.NDEF_DISCOVERED" /> 183 | * <category android:name="android.intent.category.DEFAULT" /> 184 | * <data android:scheme="http" 185 | * android:host="example.com" 186 | * android:path="/funapp" /> 187 | * </intent-filter> 188 | * </activity> 189 | *190 | * 191 | * For devices supporting SDK 14 and above, the handover record also includes 192 | * an Android Application Record, allowing your application to be discovered in 193 | * the market if it is not yet installed. Otherwise, the uri provided by 194 | * getAppReference() should direct the user to a web page relevant to your 195 | * application. 196 | */ 197 | public abstract class BluetoothConnector { 198 | private static final String SERVICE_NAME = "NfcBtHandover"; 199 | private static final String BT_SOCKET_SCHEMA = "btsocket://"; 200 | private static final String TAG = "btconnect"; 201 | private static final boolean DBG = false; 202 | 203 | /** 204 | * Configures the {@link mobisocial.nfc.Nfc} interface to set up a Bluetooth 205 | * socket with another device. The method both sets the foreground ndef 206 | * messages and registers an {@link mobisocial.nfc.NdefHandler} to look for 207 | * incoming pairing requests. 208 | * 209 | *
When this method is called, a Bluetooth server socket is created, 210 | * and the socket is closed after a successful connection. You must call 211 | * prepare() again to reinitiate the server socket. 212 | * 213 | * @return The server socket listening for peers. 214 | */ 215 | public static BluetoothServerSocket prepare(Nfc nfc, OnConnectedListener conn) { 216 | BluetoothConnecting btConnecting = new BluetoothConnecting(conn); 217 | nfc.getConnectionHandoverManager().addConnectionHandover(btConnecting); 218 | nfc.share(btConnecting.getHandoverRequestMessage(nfc.getContext())); 219 | return btConnecting.mAcceptThread.mmServerSocket; 220 | } 221 | 222 | /** 223 | * Configures the {@link mobisocial.nfc.Nfc} interface to set up a Bluetooth 224 | * socket with another device. The method both sets the foreground ndef 225 | * messages and registers an {@link mobisocial.nfc.NdefHandler} to look for 226 | * incoming pairing requests. 227 | * 228 | *
When this method is called, a Bluetooth server socket is created, 229 | * and the socket is closed after a successful connection. You must call 230 | * prepare() again to reinitiate the server socket. 231 | * 232 | * @return The server socket listening for peers. 233 | */ 234 | public static BluetoothServerSocket prepare(Nfc nfc, OnConnectedListener conn, NdefRecord[] ndef) { 235 | BluetoothConnecting btConnecting = new BluetoothConnecting(conn); 236 | NdefMessage handoverRequest = btConnecting.getHandoverRequestMessage(nfc.getContext()); 237 | NdefRecord[] combinedRecords = new NdefRecord[ndef.length + handoverRequest.getRecords().length]; 238 | 239 | int i = 0; 240 | for (NdefRecord r : ndef) { 241 | combinedRecords[i++] = r; 242 | } 243 | for (NdefRecord r : handoverRequest.getRecords()) { 244 | combinedRecords[i++] = r; 245 | } 246 | 247 | NdefMessage outbound = new NdefMessage(combinedRecords); 248 | nfc.getConnectionHandoverManager().addConnectionHandover(btConnecting); 249 | nfc.share(outbound); 250 | return btConnecting.mAcceptThread.mmServerSocket; 251 | } 252 | 253 | /** 254 | * Extracts the Bluetooth socket information from an ndef message and 255 | * connects as a client. 256 | */ 257 | public static void join(Nfc nfc, OnConnectedListener conn, NdefMessage ndef) { 258 | BluetoothConnecting btConnecting = new BluetoothConnecting(conn, true, UUID.randomUUID()); 259 | ConnectionHandoverManager manager = new ConnectionHandoverManager(); 260 | manager.addConnectionHandover(btConnecting); 261 | manager.doHandover(ndef); 262 | } 263 | 264 | private static class BluetoothConnecting implements ConnectionHandover { 265 | private final AcceptThread mAcceptThread; 266 | private final byte[] mCollisionResolution; 267 | private final OnConnectedListener mmBtConnected; 268 | private final BluetoothAdapter mBluetoothAdapter; 269 | private final UUID mServiceUuid; 270 | private final int mChannel; 271 | private final boolean mAlwaysClient; 272 | private boolean mConnectionStarted; 273 | 274 | public BluetoothConnecting(OnConnectedListener onBtConnected, boolean alwaysClient, 275 | UUID serviceUuid) { 276 | mAlwaysClient = alwaysClient; 277 | mmBtConnected = onBtConnected; 278 | mServiceUuid = serviceUuid; 279 | mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 280 | if (mBluetoothAdapter == null) { 281 | throw new IllegalStateException("No Bluetooth adapter found."); 282 | } 283 | Random random = new Random(); 284 | mCollisionResolution = new byte[2]; 285 | random.nextBytes(mCollisionResolution); 286 | 287 | mAcceptThread = new AcceptThread(); 288 | mChannel = mAcceptThread.getListeningPort(); 289 | mAcceptThread.start(); 290 | } 291 | 292 | public BluetoothConnecting(OnConnectedListener onBtConnected) { 293 | this(onBtConnected, false, UUID.randomUUID()); 294 | } 295 | 296 | private NdefMessage getHandoverRequestMessage(Context context) { 297 | NdefRecord[] records = new NdefRecord[4]; 298 | 299 | /* Handover Request */ 300 | byte[] version = new byte[] { 301 | (0x1 << 4) | (0x2) 302 | }; 303 | records[0] = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_REQUEST, 304 | new byte[0], version); 305 | 306 | /* Collision Resolution */ 307 | records[1] = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, new byte[] { 308 | 0x63, 0x72 309 | }, new byte[0], mCollisionResolution); 310 | 311 | /* Handover record */ 312 | StringBuilder btRequest = new StringBuilder(BT_SOCKET_SCHEMA) 313 | .append(mBluetoothAdapter.getAddress()) 314 | .append("/") 315 | .append(mServiceUuid); 316 | if (mChannel != -1) { 317 | btRequest.append("?channel=" + mChannel); 318 | } 319 | records[2] = new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI, NdefRecord.RTD_URI, 320 | new byte[0], btRequest.toString().getBytes()); 321 | 322 | records[3] = NdefFactory.createApplicationRecord(context.getPackageName()); 323 | NdefMessage ndef = new NdefMessage(records); 324 | return ndef; 325 | } 326 | 327 | @Override 328 | public void doConnectionHandover(NdefMessage handoverRequest, int handover, int record) 329 | throws IOException { 330 | 331 | byte[] remoteCollision = handoverRequest.getRecords()[handover + 1].getPayload(); 332 | if (remoteCollision[0] == mCollisionResolution[0] 333 | && remoteCollision[1] == mCollisionResolution[1]) { 334 | return; // They'll have to try again. 335 | } 336 | boolean amServer = (remoteCollision[0] < mCollisionResolution[0] || 337 | (remoteCollision[0] == mCollisionResolution[0] && remoteCollision[1] < mCollisionResolution[1])); 338 | 339 | if (mAlwaysClient) { 340 | amServer = false; 341 | } 342 | 343 | if (!mConnectionStarted) { 344 | synchronized(BluetoothConnecting.this) { 345 | if (!mConnectionStarted) { 346 | mConnectionStarted = true; 347 | mmBtConnected.beforeConnect(amServer); 348 | } 349 | } 350 | } 351 | if (!amServer) { 352 | // Not waiting for a connection: 353 | mAcceptThread.cancel(); 354 | Uri uri = Uri.parse(new String(handoverRequest.getRecords()[record].getPayload())); 355 | UUID serviceUuid = UUID.fromString(uri.getPath().substring(1)); 356 | int channel = -1; 357 | String channelStr = uri.getQueryParameter("channel"); 358 | if (null != channelStr) { 359 | channel = Integer.parseInt(channelStr); 360 | } 361 | 362 | BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteDevice(uri.getAuthority()); 363 | new ConnectThread(remoteDevice, serviceUuid, channel).start(); 364 | } 365 | } 366 | 367 | @Override 368 | public boolean supportsRequest(NdefRecord handoverRequest) { 369 | if (handoverRequest.getTnf() != NdefRecord.TNF_ABSOLUTE_URI 370 | || !Arrays.equals(handoverRequest.getType(), NdefRecord.RTD_URI)) { 371 | return false; 372 | } 373 | 374 | String uriString = new String(handoverRequest.getPayload()); 375 | if (uriString.startsWith(BT_SOCKET_SCHEMA)) { 376 | return true; 377 | } 378 | 379 | return false; 380 | } 381 | 382 | private class ConnectThread extends Thread { 383 | private final BluetoothSocket mmSocket; 384 | private final BluetoothDevice mmDevice; 385 | 386 | public ConnectThread(BluetoothDevice device, UUID uuid, int channel) { 387 | mmDevice = device; 388 | 389 | BluetoothSocket tmp = null; 390 | try { 391 | tmp = createBluetoothSocket(mmDevice, uuid, channel); 392 | } catch (IOException e) { 393 | Log.e(TAG, "create() failed", e); 394 | } 395 | mmSocket = tmp; 396 | } 397 | 398 | public void run() { 399 | setName("ConnectThread"); 400 | 401 | try { 402 | mmSocket.connect(); 403 | } catch (IOException e) { 404 | Log.e(TAG, "failed to connect to bluetooth socket", e); 405 | try { 406 | mmSocket.close(); 407 | } catch (IOException e2) { 408 | Log.e(TAG, "unable to close() socket during connection failure", e2); 409 | } 410 | 411 | return; 412 | } 413 | 414 | mmBtConnected.onConnectionEstablished(mmSocket, false); 415 | } 416 | } 417 | 418 | private class AcceptThread extends Thread { 419 | // The local server socket 420 | private final BluetoothServerSocket mmServerSocket; 421 | private final int mmListeningPort; 422 | 423 | private AcceptThread() { 424 | BluetoothServerSocket tmp = null; 425 | 426 | // Create a new listening server socket 427 | int listeningPort = -1; 428 | try { 429 | tmp = getBluetoothServerSocket(); 430 | listeningPort = getBluetoothListeningPort(tmp); 431 | } catch (IOException e) { 432 | Log.e(TAG, "listen() failed", e); 433 | } 434 | mmListeningPort = listeningPort; 435 | mmServerSocket = tmp; 436 | } 437 | 438 | public void run() { 439 | setName("AcceptThread"); 440 | BluetoothSocket socket = null; 441 | 442 | // Wait for one connection. 443 | try { 444 | // This is a blocking call and will only return on a 445 | // successful connection or an exception 446 | socket = mmServerSocket.accept(); 447 | if (!mConnectionStarted) { 448 | synchronized(BluetoothConnecting.this) { 449 | if (!mConnectionStarted) { 450 | mConnectionStarted = true; 451 | mmBtConnected.beforeConnect(true); 452 | } 453 | } 454 | } 455 | } catch (IOException e) { 456 | if (DBG) Log.e(TAG, "accept() failed", e); 457 | return; 458 | } 459 | 460 | if (socket == null) { 461 | return; 462 | } 463 | 464 | try { 465 | mmServerSocket.close(); 466 | } catch (IOException e) { 467 | } 468 | mmBtConnected.onConnectionEstablished(socket, true); 469 | } 470 | 471 | public void cancel() { 472 | try { 473 | mmServerSocket.close(); 474 | } catch (IOException e) { 475 | } 476 | } 477 | 478 | public int getListeningPort() { 479 | return mmListeningPort; 480 | } 481 | } 482 | 483 | private BluetoothServerSocket getBluetoothServerSocket() throws IOException { 484 | BluetoothServerSocket tmp; 485 | 486 | if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD_MR1) { 487 | tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(SERVICE_NAME, 488 | mServiceUuid); 489 | if (DBG) Log.d(TAG, "Using secure bluetooth server socket"); 490 | } else { 491 | try { 492 | // compatibility with pre SDK 10 devices 493 | Method listener = mBluetoothAdapter.getClass().getMethod( 494 | "listenUsingInsecureRfcommWithServiceRecord", String.class, UUID.class); 495 | tmp = (BluetoothServerSocket) listener.invoke(mBluetoothAdapter, SERVICE_NAME, mServiceUuid); 496 | if (DBG) Log.d(TAG, "Using insecure bluetooth server socket"); 497 | } catch (NoSuchMethodException e) { 498 | Log.wtf(TAG, "listenUsingInsecureRfcommWithServiceRecord not found"); 499 | throw new IOException(e); 500 | } catch (InvocationTargetException e) { 501 | Log.wtf(TAG, 502 | "listenUsingInsecureRfcommWithServiceRecord not available on mBtAdapter"); 503 | throw new IOException(e); 504 | } catch (IllegalAccessException e) { 505 | Log.wtf(TAG, 506 | "listenUsingInsecureRfcommWithServiceRecord not available on mBtAdapter"); 507 | throw new IOException(e); 508 | } 509 | } 510 | return tmp; 511 | } 512 | 513 | private BluetoothSocket createBluetoothSocket(BluetoothDevice device, UUID uuid, int channel) 514 | throws IOException { 515 | 516 | BluetoothSocket tmp; 517 | 518 | if (channel != -1) { 519 | try { 520 | if (DBG) Log.d(TAG, "trying to connect to channel " + channel); 521 | Method listener = device.getClass().getMethod("createInsecureRfcommSocket", int.class); 522 | return (BluetoothSocket) listener.invoke(device, channel); 523 | } catch (Exception e) { 524 | if (DBG) Log.w(TAG, "Could not connect to channel.", e); 525 | } 526 | } 527 | 528 | if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD_MR1) { 529 | tmp = device.createRfcommSocketToServiceRecord(uuid); 530 | if (DBG) Log.d(TAG, "Using secure bluetooth socket"); 531 | } else { 532 | try { 533 | // compatibility with pre SDK 10 devices 534 | Method listener = device.getClass().getMethod( 535 | "createInsecureRfcommSocketToServiceRecord", UUID.class); 536 | tmp = (BluetoothSocket) listener.invoke(device, uuid); 537 | if (DBG) Log.d(TAG, "Using insecure bluetooth socket"); 538 | } catch (NoSuchMethodException e) { 539 | Log.wtf(TAG, "createInsecureRfcommSocketToServiceRecord not found"); 540 | throw new IOException(e); 541 | } catch (InvocationTargetException e) { 542 | Log.wtf(TAG, 543 | "createInsecureRfcommSocketToServiceRecord not available on mBtAdapter"); 544 | throw new IOException(e); 545 | } catch (IllegalAccessException e) { 546 | Log.wtf(TAG, 547 | "createInsecureRfcommSocketToServiceRecord not available on mBtAdapter"); 548 | throw new IOException(e); 549 | } 550 | } 551 | return tmp; 552 | } 553 | 554 | private static int getBluetoothListeningPort(BluetoothServerSocket serverSocket) { 555 | try { 556 | Field socketField = BluetoothServerSocket.class.getDeclaredField("mSocket"); 557 | socketField.setAccessible(true); 558 | BluetoothSocket socket = (BluetoothSocket)socketField.get(serverSocket); 559 | 560 | Field portField = BluetoothSocket.class.getDeclaredField("mPort"); 561 | portField.setAccessible(true); 562 | int port = (Integer)portField.get(socket); 563 | return port; 564 | } catch (Exception e) { 565 | Log.d(TAG, "Error getting port from socket", e); 566 | return -1; 567 | } 568 | } 569 | } 570 | 571 | /** 572 | * A callback used when a Bluetooth connection has been established. 573 | */ 574 | public interface OnConnectedListener { 575 | /** 576 | * The method called when a Bluetooth connection has been established. 577 | * 578 | * @param socket The connected Bluetooth socket. 579 | * @param isServer True if this connection is the "host" of this 580 | * connection. Useful in establishing an asymmetric 581 | * relationship between otherwise symmetric devices. 582 | */ 583 | public void onConnectionEstablished(BluetoothSocket socket, boolean isServer); 584 | 585 | /** 586 | * Called before an attempt to set up a Bluetooth connection. 587 | * @param isServer True if this connection is the "host" of this 588 | * connection. Useful in establishing an asymmetric 589 | * relationship between otherwise symmetric devices. 590 | */ 591 | public void beforeConnect(boolean isServer); 592 | } 593 | } 594 | --------------------------------------------------------------------------------