├── 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 | 4 | 4.0.0 5 | 6 | org.mobisocial 7 | easynfc 8 | 0.3.4-SNAPSHOT 9 | jar 10 | EasyNfc 11 | An abstraction layer for Nfc on Android 12 | 13 | 14 | com.google.android 15 | android 16 | 2.3.3 17 | provided 18 | 19 | 20 | 21 | 22 | 23 | com.jayway.maven.plugins.android.generation2 24 | maven-android-plugin 25 | 2.8.4 26 | 27 | 28 | /usr/share/android-sdk-linux_86 29 | 10 30 | 31 | true 32 | 33 | true 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-compiler-plugin 38 | 39 | 1.6 40 | 1.6 41 | true 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-javadoc-plugin 51 | 2.7 52 | 53 | 54 | com.google.doclava 55 | doclava 56 | 1.0.4-SNAPSHOT 57 | 58 | com.google.doclava.Doclava 59 | 62 | ${sun.boot.class.path} 63 | 64 | -quiet 65 | -federate Android http://developer.android.com/reference 66 | -federationxml Android http://doclava.googlecode.com/svn/static/api/android-10.xml 67 | -hdf project.name "${project.name}" 68 | -d ${project.build.directory}/apidocs 69 | -generatesources 70 | 71 | false 72 | 75 | -J-Xmx1024m 76 | 77 | 78 | 79 | 80 | 81 | 82 | mobisocial-releases 83 | Internal Releases 84 | http://mobisocial.stanford.edu:8081/nexus/content/repositories/releases 85 | 86 | 87 | mobisocial-snapshots 88 | Internal Snapshots 89 | http://mobisocial.stanford.edu:8081/nexus/content/repositories/snapshots 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/main/java/mobisocial/ndefexchange/NdefTcpPushHandover.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.net.Socket; 24 | import java.net.URI; 25 | import java.util.Arrays; 26 | 27 | import mobisocial.comm.DuplexSocket; 28 | import mobisocial.comm.TcpDuplexSocket; 29 | import mobisocial.nfc.ConnectionHandover; 30 | import mobisocial.nfc.NdefFactory; 31 | 32 | 33 | import android.net.Uri; 34 | import android.nfc.FormatException; 35 | import android.nfc.NdefMessage; 36 | import android.nfc.NdefRecord; 37 | import android.util.Log; 38 | 39 | /** 40 | *

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 CREATOR = 117 | new Parcelable.Creator() { 118 | @Override 119 | public NdefMessage createFromParcel(Parcel in) { 120 | int recordsLength = in.readInt(); 121 | NdefRecord[] records = new NdefRecord[recordsLength]; 122 | in.readTypedArray(records, NdefRecord.CREATOR); 123 | return new NdefMessage(records); 124 | } 125 | @Override 126 | public NdefMessage[] newArray(int size) { 127 | return new NdefMessage[size]; 128 | } 129 | }; 130 | 131 | private native int parseNdefMessage(byte[] data); 132 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/ConnectionHandoverManager.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 | import java.util.Arrays; 22 | import java.util.Iterator; 23 | import java.util.LinkedHashSet; 24 | import java.util.Set; 25 | 26 | import android.net.Uri; 27 | import android.nfc.FormatException; 28 | import android.nfc.NdefMessage; 29 | import android.nfc.NdefRecord; 30 | import android.util.Log; 31 | 32 | public class ConnectionHandoverManager implements NdefHandler, PrioritizedHandler { 33 | public static final String USER_HANDOVER_PREFIX = "ndef://wkt:hr/"; 34 | public static final String TAG = "connectionhandover"; 35 | public static final int HANDOVER_PRIORITY = 5; 36 | private final Set mmConnectionHandovers = new LinkedHashSet(); 37 | 38 | public ConnectionHandoverManager() { 39 | 40 | } 41 | 42 | public void addConnectionHandover(ConnectionHandover handover) { 43 | mmConnectionHandovers.add(handover); 44 | } 45 | 46 | public void clearConnectionHandovers() { 47 | mmConnectionHandovers.clear(); 48 | } 49 | 50 | public boolean removeConnectionHandover(ConnectionHandover handover) { 51 | return mmConnectionHandovers.remove(handover); 52 | } 53 | 54 | /** 55 | * Returns the (mutable) set of active connection handover 56 | * responders. 57 | */ 58 | public Set getConnectionHandoverResponders() { 59 | return mmConnectionHandovers; 60 | } 61 | 62 | //@Override 63 | public final int handleNdef(NdefMessage[] handoverRequest) { 64 | // TODO: What does this mean? 65 | return doHandover(handoverRequest[0]); 66 | } 67 | 68 | public final int doHandover(NdefMessage handoverRequest) { 69 | int handoverRecordPos = findHandoverRequest(handoverRequest); 70 | if (handoverRecordPos == -1) { 71 | return NDEF_PROPAGATE; 72 | } 73 | 74 | if (findUserspaceHandoverRequest(handoverRequest) != -1) { 75 | Uri uri; 76 | try { 77 | uri = NdefFactory.parseUri(handoverRequest.getRecords()[0]); 78 | } catch (FormatException e) { 79 | Log.e(TAG, "Bad handover record.", e); 80 | return NDEF_PROPAGATE; 81 | } 82 | handoverRequest = NdefFactory.fromNdefUri(uri); 83 | handoverRecordPos = 0; 84 | } 85 | 86 | NdefRecord[] records = handoverRequest.getRecords(); 87 | for (int i = handoverRecordPos + 2; i < records.length; i++) { 88 | Iterator handovers = mmConnectionHandovers.iterator(); 89 | while (handovers.hasNext()) { 90 | ConnectionHandover handover = handovers.next(); 91 | if (handover.supportsRequest(records[i])) { 92 | try { 93 | Log.d(TAG, "Attempting handover " + handover); 94 | handover.doConnectionHandover(handoverRequest, handoverRecordPos, i); 95 | return NDEF_CONSUME; 96 | } catch (IOException e) { 97 | Log.w(TAG, "Handover failed.", e); 98 | // try the next one. 99 | } 100 | } 101 | } 102 | } 103 | 104 | return NDEF_PROPAGATE; 105 | } 106 | 107 | //@Override 108 | public int getPriority() { 109 | return HANDOVER_PRIORITY; 110 | } 111 | 112 | /** 113 | * Returns true if the given Ndef message contains a connection 114 | * handover request. 115 | */ 116 | public static int findHandoverRequest(NdefMessage ndef) { 117 | NdefRecord[] records = (ndef).getRecords(); 118 | 119 | for (int i = 0; i < records.length; i++) { 120 | if (records[i].getTnf() == NdefRecord.TNF_WELL_KNOWN 121 | && Arrays.equals(records[i].getType(), NdefRecord.RTD_HANDOVER_REQUEST)) { 122 | return i; 123 | } 124 | } 125 | return findUserspaceHandoverRequest(ndef); 126 | } 127 | 128 | public static boolean isHandoverRequest(NdefMessage ndefMessage) { 129 | NdefRecord ndef = ndefMessage.getRecords()[0]; 130 | return (ndef.getTnf() == NdefRecord.TNF_WELL_KNOWN 131 | && Arrays.equals(ndef.getType(), NdefRecord.RTD_HANDOVER_REQUEST)); 132 | } 133 | // User-space handover: 134 | // TODO: Support uri profile 135 | private static int findUserspaceHandoverRequest(NdefMessage ndef) { 136 | NdefRecord[] records = (ndef).getRecords(); 137 | for (int i = 0; i < records.length; i++) { 138 | try { 139 | String uriStr = NdefFactory.parseUri(records[i]).toString(); 140 | if (uriStr.startsWith(USER_HANDOVER_PREFIX)) { 141 | return i; 142 | } 143 | } catch (FormatException e) { } 144 | } 145 | return -1; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/NdefFactory.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.UnsupportedEncodingException; 21 | import java.net.URI; 22 | import java.net.URL; 23 | import java.util.Arrays; 24 | 25 | import android.net.Uri; 26 | import android.nfc.FormatException; 27 | import android.nfc.NdefMessage; 28 | import android.nfc.NdefRecord; 29 | import android.util.Base64; 30 | 31 | /** 32 | * A utility class for generating NDEF messages. 33 | * @see NdefMessage 34 | */ 35 | public class NdefFactory { 36 | /** 37 | * An RTD indicating an Android Application Record. 38 | */ 39 | public static final byte[] RTD_ANDROID_APP = "android.com:pkg".getBytes(); 40 | 41 | private static final String[] URI_PREFIXES = new String[] { 42 | "", 43 | "http://www.", 44 | "https://www.", 45 | "http://", 46 | "https://", 47 | "tel:", 48 | "mailto:", 49 | "ftp://anonymous:anonymous@", 50 | "ftp://ftp.", 51 | "ftps://", 52 | "sftp://", 53 | "smb://", 54 | "nfs://", 55 | "ftp://", 56 | "dav://", 57 | "news:", 58 | "telnet://", 59 | "imap:", 60 | "rtsp://", 61 | "urn:", 62 | "pop:", 63 | "sip:", 64 | "sips:", 65 | "tftp:", 66 | "btspp://", 67 | "btl2cap://", 68 | "btgoep://", 69 | "tcpobex://", 70 | "irdaobex://", 71 | "file://", 72 | "urn:epc:id:", 73 | "urn:epc:tag:", 74 | "urn:epc:pat:", 75 | "urn:epc:raw:", 76 | "urn:epc:", 77 | "urn:nfc:", 78 | }; 79 | 80 | public static NdefMessage fromUri(Uri uri) { 81 | return fromUri(uri.toString()); 82 | } 83 | 84 | public static NdefMessage fromUri(URI uri) { 85 | return fromUri(uri.toString()); 86 | } 87 | 88 | public static NdefMessage fromUri(String uri) { 89 | try { 90 | int prefix = 0; 91 | for (int i = 1; i < URI_PREFIXES.length; i++) { 92 | if (uri.startsWith(URI_PREFIXES[i])) { 93 | prefix = i; 94 | break; 95 | } 96 | } 97 | if (prefix > 0) uri = uri.substring(URI_PREFIXES[prefix].length()); 98 | int len = uri.length(); 99 | byte[] payload = new byte[len + 1]; 100 | payload[0] = (byte) prefix; 101 | System.arraycopy(uri.getBytes("UTF-8"), 0, payload, 1, len); 102 | NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_URI, 103 | new byte[0], payload); 104 | NdefRecord[] records = new NdefRecord[] { record }; 105 | return new NdefMessage(records); 106 | } catch (NoClassDefFoundError e) { 107 | return null; 108 | } catch (UnsupportedEncodingException e) { 109 | // no UTF-8? really? 110 | return null; 111 | } 112 | } 113 | 114 | public static NdefMessage fromUrl(URL url) { 115 | return fromUri(url.toString()); 116 | } 117 | 118 | public static NdefMessage fromMime(String mimeType, byte[] data) { 119 | try { 120 | NdefRecord record = new NdefRecord(NdefRecord.TNF_MIME_MEDIA, 121 | mimeType.getBytes(), new byte[0], data); 122 | NdefRecord[] records = new NdefRecord[] { record }; 123 | return new NdefMessage(records); 124 | } catch (NoClassDefFoundError e) { 125 | return null; 126 | } 127 | } 128 | 129 | /** 130 | * Creates an NDEF message with a single text record, with language 131 | * code "en" and the given text, encoded using UTF-8. 132 | */ 133 | public static NdefMessage fromText(String text) { 134 | try { 135 | byte[] textBytes = text.getBytes(); 136 | byte[] textPayload = new byte[textBytes.length + 3]; 137 | textPayload[0] = 0x02; // Status byte; UTF-8 and "en" encoding. 138 | textPayload[1] = 'e'; 139 | textPayload[2] = 'n'; 140 | System.arraycopy(textBytes, 0, textPayload, 3, textBytes.length); 141 | NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, 142 | NdefRecord.RTD_TEXT, new byte[0], textPayload); 143 | NdefRecord[] records = new NdefRecord[] { record }; 144 | return new NdefMessage(records); 145 | } catch (NoClassDefFoundError e) { 146 | return null; 147 | } 148 | } 149 | 150 | /** 151 | * Creates an NDEF message with a single text record, with the given 152 | * text content (UTF-8 encoded) and language code. 153 | */ 154 | public static NdefMessage fromText(String text, String languageCode) { 155 | try { 156 | int languageCodeLength = languageCode.length(); 157 | int textLength = text.length(); 158 | byte[] textPayload = new byte[textLength + 1 + languageCodeLength]; 159 | textPayload[0] = (byte)(0x3F & languageCodeLength); // UTF-8 with the given language code length. 160 | System.arraycopy(languageCode.getBytes(), 0, textPayload, 1, languageCodeLength); 161 | System.arraycopy(text.getBytes(), 0, textPayload, 1 + languageCodeLength, textLength); 162 | NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, 163 | NdefRecord.RTD_TEXT, new byte[0], textPayload); 164 | NdefRecord[] records = new NdefRecord[] { record }; 165 | return new NdefMessage(records); 166 | } catch (NoClassDefFoundError e) { 167 | return null; 168 | } 169 | } 170 | 171 | public static final NdefMessage getEmptyNdef() { 172 | byte[] empty = new byte[] {}; 173 | NdefRecord[] records = new NdefRecord[1]; 174 | records[0] = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, empty, empty, empty); 175 | NdefMessage ndef = new NdefMessage(records); 176 | return ndef; 177 | } 178 | 179 | public static final boolean isEmpty(NdefMessage ndef) { 180 | return (ndef == null || ndef.equals(getEmptyNdef())); 181 | } 182 | 183 | /** 184 | * Converts an Ndef message encoded in uri format to an NdefMessage. 185 | * {@hide} 186 | */ 187 | public static final NdefMessage fromNdefUri(Uri uri) { 188 | if (!"ndef".equals(uri.getScheme())) { 189 | throw new IllegalArgumentException("Not an ndef:// uri. did you mean NdefFactory.fromUri()?"); 190 | } 191 | NdefMessage wrappedNdef; 192 | try { 193 | wrappedNdef = new NdefMessage(android.util.Base64.decode( 194 | uri.getPath().substring(1), Base64.URL_SAFE)); 195 | } catch (FormatException e) { 196 | throw new IllegalArgumentException("Format error."); 197 | } 198 | return wrappedNdef; 199 | } 200 | 201 | public static final Uri parseUri(NdefRecord record) throws FormatException { 202 | int tnf = record.getTnf(); 203 | if (tnf == NdefRecord.TNF_ABSOLUTE_URI) { 204 | return Uri.parse(new String(record.getType())); 205 | } 206 | if (tnf == NdefRecord.TNF_WELL_KNOWN && 207 | Arrays.equals(NdefRecord.RTD_URI, record.getType())) { 208 | byte[] payload = record.getPayload(); 209 | int pre = (int)payload[0]; 210 | if (!(pre >= 0 && pre < URI_PREFIXES.length)) { 211 | throw new FormatException("Unknown uri prefix: " + pre); 212 | } 213 | String prefix = URI_PREFIXES[pre]; 214 | String uriStr = new StringBuilder() 215 | .append(prefix).append(new String(payload, 1, payload.length - 1)) 216 | .toString(); 217 | return Uri.parse(uriStr); 218 | } 219 | throw new FormatException("NdefRecord is not a uri."); 220 | } 221 | 222 | /** 223 | * Converts an Ndef message to its uri encoding, using the 224 | * {code ndef://} scheme. 225 | */ 226 | // TODO Switch for the appropriate type 227 | /* 228 | public static final Uri toNdefUri(NdefMessage ndef) { 229 | return Uri.parse("ndef://url/" + Base64.encodeToString(ndef.toByteArray(), Base64.URL_SAFE)); 230 | }*/ 231 | 232 | /** 233 | *

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 | *

34 | *

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 CREATOR = 272 | new Parcelable.Creator() { 273 | public NdefRecord createFromParcel(Parcel in) { 274 | short tnf = (short)in.readInt(); 275 | int typeLength = in.readInt(); 276 | byte[] type = new byte[typeLength]; 277 | in.readByteArray(type); 278 | int idLength = in.readInt(); 279 | byte[] id = new byte[idLength]; 280 | in.readByteArray(id); 281 | int payloadLength = in.readInt(); 282 | byte[] payload = new byte[payloadLength]; 283 | in.readByteArray(payload); 284 | 285 | return new NdefRecord(tnf, type, id, payload); 286 | } 287 | public NdefRecord[] newArray(int size) { 288 | return new NdefRecord[size]; 289 | } 290 | }; 291 | 292 | private int parseNdefRecord(byte[] data) { 293 | // -1 for error 294 | ByteBuffer buffer = ByteBuffer.wrap(data); 295 | byte header = buffer.get(); 296 | this.mFlags = (byte)(header & 0x7c); 297 | this.mTnf = (byte)(header & 0x07); 298 | 299 | byte typeLength; 300 | int payloadLength; 301 | byte idLength = 0x00; 302 | 303 | typeLength = buffer.get(); 304 | if ((mFlags & FLAG_SR) > 0) { 305 | // short record 306 | payloadLength = 0xFF & buffer.get(); 307 | } else { 308 | payloadLength = buffer.getInt(); 309 | } 310 | if ((mFlags & FLAG_IL) > 0) { 311 | idLength = buffer.get(); 312 | } 313 | 314 | mType = new byte[typeLength]; 315 | buffer.get(mType); 316 | 317 | mId = new byte[idLength]; 318 | buffer.get(mId); 319 | 320 | mPayload = new byte[payloadLength]; 321 | buffer.get(mPayload); 322 | 323 | return 0; 324 | } 325 | private byte[] generate(short flags, short tnf, byte[] type, byte[] id, byte[] data) { 326 | if (data.length < 255) { 327 | flags = (short)(flags | FLAG_SR); 328 | } 329 | 330 | boolean idPresent = (flags & FLAG_IL) != 0; 331 | boolean shortRecord = (flags & FLAG_SR) != 0; 332 | int fixedBytes = (idPresent) ? 7 : 6; 333 | if (shortRecord) { 334 | fixedBytes -= 3; 335 | } 336 | ByteBuffer record = ByteBuffer.allocate(fixedBytes + id.length + type.length + data.length); 337 | record.put((byte)(flags | tnf)); 338 | record.put((byte)(0xFF & type.length)); 339 | 340 | if (!shortRecord) { 341 | record.put((byte)(0xFF & (data.length >>> 24))); 342 | record.put((byte)(0xFF & (data.length >>> 16))); 343 | record.put((byte)((0xFF & data.length >>> 8))); 344 | } 345 | record.put((byte)((0xFF & data.length))); 346 | 347 | if (idPresent) { 348 | record.put((byte)id.length); 349 | } 350 | record.put(type); 351 | record.put(id); 352 | record.put(data); 353 | return record.array(); 354 | } 355 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/Nfc.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 | import java.net.URL; 22 | import java.util.HashSet; 23 | import java.util.Iterator; 24 | import java.util.Map; 25 | import java.util.Set; 26 | import java.util.TreeMap; 27 | 28 | import mobisocial.ndefexchange.NdefExchangeContract; 29 | import mobisocial.ndefexchange.NdefExchangeManager; 30 | 31 | import android.app.Activity; 32 | import android.app.PendingIntent; 33 | import android.content.BroadcastReceiver; 34 | import android.content.Context; 35 | import android.content.Intent; 36 | import android.content.IntentFilter; 37 | import android.content.pm.PackageManager; 38 | import android.net.Uri; 39 | import android.nfc.NdefMessage; 40 | import android.nfc.NfcAdapter; 41 | import android.nfc.Tag; 42 | import android.nfc.tech.Ndef; 43 | import android.nfc.tech.NdefFormatable; 44 | import android.os.Build; 45 | import android.os.Parcelable; 46 | import android.util.Log; 47 | import android.widget.Toast; 48 | 49 | /** 50 | *

This class acts as an abstraction layer for Android's Nfc stack. 51 | * The goals of this project are to: 52 | *

58 | *

59 | * 60 | *

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 | *

64 | *
 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 | *

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 | *

99 | *

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 | *

109 | *

110 | */ 111 | public class Nfc { 112 | private static final String TAG = "easynfc"; 113 | 114 | private Activity mActivity; 115 | private NfcAdapter mNfcAdapter; 116 | private final IntentFilter[] mIntentFilters; 117 | private final String[][] mTechLists; 118 | private NdefMessage mForegroundMessage = null; 119 | private NdefMessage mWriteMessage = null; 120 | private final Map> mNdefHandlers = new TreeMap>(); 121 | private boolean mHandoverEnabled = true; 122 | private OnTagWriteListener mOnTagWriteListener = null; 123 | private final ConnectionHandoverManager mNdefExchangeManager; 124 | 125 | private int mState = STATE_PAUSED; 126 | private int mInterfaceMode = MODE_EXCHANGE; 127 | 128 | private static final int STATE_PAUSED = 0; 129 | private static final int STATE_PAUSING = 1; 130 | private static final int STATE_RESUMING = 2; 131 | private static final int STATE_RESUMED = 3; 132 | 133 | /** 134 | * A broadcasted intent used to set an NDEF message for use in a Connection 135 | * Handover, for devices that do not have an active NFC radio. 136 | */ 137 | protected static final String ACTION_SET_NDEF = "mobisocial.intent.action.SET_NDEF"; 138 | 139 | /** 140 | * The action of an ordered broadcast intent for applications to handle a 141 | * received NDEF messages. Such intents are broadcast from connection 142 | * handover services. This library sets the result code to 143 | * {@code Activity.RESULT_CANCELED}, indicating the foreground application has 144 | * consumed the intent. 145 | */ 146 | protected static final String ACTION_HANDLE_NDEF = "mobisocial.intent.action.HANDLE_NDEF"; 147 | 148 | /** 149 | * Nfc interface mode in which Nfc interaction is disabled for this class. 150 | */ 151 | public static final int MODE_PASSTHROUGH = 0; 152 | 153 | /** 154 | * Nfc interface mode for reading data from a passive tag 155 | * or exchanging information with another active device. 156 | * See {@link #addNdefHandler(NdefHandler)} and 157 | * {@link #share(NdefMessage)} for handling the actual data. 158 | */ 159 | public static final int MODE_EXCHANGE = 1; 160 | 161 | /** 162 | * Nfc interface mode for writing data to a passive tag. 163 | */ 164 | public static final int MODE_WRITE = 2; 165 | 166 | 167 | public Nfc(Activity activity, IntentFilter[] intentFilters, String[][] techLists) { 168 | mActivity = activity; 169 | mIntentFilters = intentFilters; 170 | mTechLists = techLists; 171 | 172 | if (Build.VERSION.SDK_INT >= NfcWrapper.SDK_NDEF_DEFINED && PackageManager.PERMISSION_GRANTED != 173 | mActivity.checkCallingOrSelfPermission("android.permission.NFC")) { 174 | 175 | throw new SecurityException("Application must hold android.permission.NFC to use libhotpotato."); 176 | } 177 | 178 | if (PackageManager.PERMISSION_GRANTED != 179 | mActivity.checkCallingOrSelfPermission("android.permission.BLUETOOTH")) { 180 | 181 | Log.w(TAG, "No android.permission.BLUETOOTH permission; bluetooth handover not supported."); 182 | } 183 | 184 | if (PackageManager.PERMISSION_GRANTED != 185 | mActivity.checkCallingOrSelfPermission("android.permission.INTERNET")) { 186 | 187 | Log.w(TAG, "No android.permission.INTERNET permission; internet handover not supported."); 188 | } 189 | 190 | if (NfcWrapper.getInstance() != null) { 191 | mNfcAdapter = NfcWrapper.getInstance().getAdapter(mActivity); 192 | } 193 | 194 | if (mNfcAdapter == null) { 195 | Log.i(TAG, "Nfc implementation not available."); 196 | } 197 | 198 | mNdefExchangeManager = new NdefExchangeManager(new NdefExchangeContract() { 199 | @Override 200 | public int handleNdef(NdefMessage[] ndef) { 201 | doHandleNdef(ndef); 202 | return NDEF_CONSUME; 203 | } 204 | 205 | @Override 206 | public NdefMessage getForegroundNdefMessage() { 207 | return mForegroundMessage; 208 | } 209 | }); 210 | addNdefHandler(mNdefExchangeManager); 211 | addNdefHandler(new EmptyNdefHandler()); 212 | } 213 | 214 | public Nfc(Activity activity) { 215 | this(activity, null, null); 216 | } 217 | 218 | /** 219 | * Returns true if this device has a native NFC implementation. 220 | */ 221 | public boolean isNativeNfcAvailable() { 222 | return mNfcAdapter != null; 223 | } 224 | 225 | /** 226 | * Removes any message from being shared with an interested reader. 227 | */ 228 | public void clearSharing() { 229 | mForegroundMessage = null; 230 | synchronized(this) { 231 | if (mState == STATE_RESUMED) { 232 | enableNdefPush(); 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Makes an ndef message available to any interested reader. 239 | * @see NdefFactory 240 | */ 241 | public void share(NdefMessage ndefMessage) { 242 | mForegroundMessage = ndefMessage; 243 | synchronized(this) { 244 | if (mState == STATE_RESUMED) { 245 | enableNdefPush(); 246 | } 247 | } 248 | } 249 | 250 | public void share(Uri uri) { 251 | mForegroundMessage = NdefFactory.fromUri(uri); 252 | synchronized(this) { 253 | if (mState == STATE_RESUMED) { 254 | enableNdefPush(); 255 | } 256 | } 257 | } 258 | 259 | public void share(URL url) { 260 | mForegroundMessage = NdefFactory.fromUrl(url); 261 | synchronized(this) { 262 | if (mState == STATE_RESUMED) { 263 | enableNdefPush(); 264 | } 265 | } 266 | } 267 | 268 | public void share (String mimeType, byte[] data) { 269 | mForegroundMessage = NdefFactory.fromMime(mimeType, data); 270 | synchronized(this) { 271 | if (mState == STATE_RESUMED) { 272 | enableNdefPush(); 273 | } 274 | } 275 | } 276 | 277 | /** 278 | * Sets a callback to call when an Nfc tag is written. 279 | */ 280 | public void setOnTagWriteListener(OnTagWriteListener listener) { 281 | mOnTagWriteListener = listener; 282 | } 283 | 284 | /** 285 | * Disallows connection handover requests. 286 | */ 287 | public void disableConnectionHandover() { 288 | mHandoverEnabled = false; 289 | } 290 | 291 | /** 292 | * Enables support for connection handover requests. 293 | */ 294 | public void enableConnectionHandover() { 295 | mHandoverEnabled = true; 296 | } 297 | 298 | /** 299 | * Returns true if connection handovers are currently supported. 300 | */ 301 | public boolean isConnectionHandoverEnabled() { 302 | return mHandoverEnabled; 303 | } 304 | 305 | /** 306 | * Sets a callback to call when an Nfc tag is written. 307 | */ 308 | public void addNdefHandler(NdefHandler handler) { 309 | if (handler instanceof PrioritizedHandler) { 310 | addNdefHandler(((PrioritizedHandler)handler).getPriority(), handler); 311 | } else { 312 | addNdefHandler(PrioritizedHandler.DEFAULT_PRIORITY, handler); 313 | } 314 | } 315 | 316 | public synchronized void addNdefHandler(Integer priority, NdefHandler handler) { 317 | if (!mNdefHandlers.containsKey(priority)) { 318 | mNdefHandlers.put(priority, new HashSet()); 319 | } 320 | Set handlers = mNdefHandlers.get(priority); 321 | handlers.add(handler); 322 | } 323 | 324 | public synchronized void clearNdefHandlers() { 325 | mNdefHandlers.clear(); 326 | } 327 | 328 | private synchronized void doHandleNdef(NdefMessage[] ndefMessages) { 329 | Iterator bins = mNdefHandlers.keySet().iterator(); 330 | while (bins.hasNext()) { 331 | Integer priority = bins.next(); 332 | Iterator handlers = mNdefHandlers.get(priority).iterator(); 333 | while (handlers.hasNext()) { 334 | NdefHandler handler = handlers.next(); 335 | if (handler.handleNdef(ndefMessages) == NdefHandler.NDEF_CONSUME) { 336 | return; 337 | } 338 | } 339 | } 340 | } 341 | 342 | /** 343 | * Interface definition for a callback called after an attempt to write 344 | * an Nfc tag. 345 | */ 346 | public interface OnTagWriteListener { 347 | public static final int WRITE_OK = 0; 348 | public static final int WRITE_ERROR_READ_ONLY = 1; 349 | public static final int WRITE_ERROR_CAPACITY = 2; 350 | public static final int WRITE_ERROR_BAD_FORMAT = 3; 351 | public static final int WRITE_ERROR_IO_EXCEPTION = 4; 352 | 353 | /** 354 | * Callback issued after an attempt to write an NFC tag. 355 | * This method is executed off the main thread, so be careful when 356 | * updating UI elements as a result of this callback. 357 | */ 358 | public void onTagWrite(int status); 359 | } 360 | 361 | /** 362 | * Puts the interface in mode {@link #MODE_WRITE}. 363 | * @param ndef The NdefMessage to write to a discovered tag. 364 | * @throws NullPointerException if ndef is null. 365 | */ 366 | public void enableTagWriteMode(NdefMessage ndef) { 367 | if (mNfcAdapter == null) { 368 | return; 369 | } 370 | 371 | if (ndef == null) { 372 | throw new NullPointerException("Cannot write null NDEF message."); 373 | } 374 | 375 | mWriteMessage = ndef; 376 | mInterfaceMode = MODE_WRITE; 377 | 378 | mActivity.runOnUiThread(new Runnable() { 379 | @Override 380 | public void run() { 381 | if (mState == STATE_RESUMED && mInterfaceMode == MODE_WRITE) { 382 | installNfcHandler(); 383 | } 384 | } 385 | }); 386 | } 387 | 388 | /** 389 | * Puts the interface in mode {@link #MODE_EXCHANGE}, 390 | * the default mode of operation for this Nfc interface. 391 | */ 392 | public void enableExchangeMode() { 393 | mInterfaceMode = MODE_EXCHANGE; 394 | mActivity.runOnUiThread(new Runnable() { 395 | @Override 396 | public void run() { 397 | if (mState == STATE_RESUMED) { 398 | installNfcHandler(); 399 | enableNdefPush(); 400 | } 401 | } 402 | }); 403 | } 404 | 405 | 406 | public void onCreate(Activity activity) { 407 | onNewIntent(activity, activity.getIntent()); 408 | } 409 | 410 | /** 411 | * Call this method in your Activity's onResume() method body. 412 | */ 413 | public void onResume(Activity activity) { 414 | // refresh mActivity 415 | mActivity = activity; 416 | 417 | mState = STATE_RESUMING; 418 | if (isConnectionHandoverEnabled()) { 419 | installNfcHandoverHandler(); 420 | enableNdefPush(); 421 | } 422 | 423 | if (mNfcAdapter != null) { 424 | synchronized(this) { 425 | if (mInterfaceMode != MODE_PASSTHROUGH) { 426 | installNfcHandler(); 427 | if (mInterfaceMode == MODE_EXCHANGE) { 428 | enableNdefPush(); 429 | } 430 | } 431 | } 432 | } 433 | mState = STATE_RESUMED; 434 | } 435 | 436 | /** 437 | * Call this method in your Activity's onPause() method body. 438 | */ 439 | public void onPause(Activity activity) { 440 | // refresh mActivity 441 | mActivity = activity; 442 | 443 | mState = STATE_PAUSING; 444 | if (isConnectionHandoverEnabled()) { 445 | uninstallNfcHandoverHandler(); 446 | notifyRemoteNfcInteface(null); 447 | } 448 | 449 | if (mNfcAdapter != null) { 450 | synchronized(this) { 451 | mNfcAdapter.disableForegroundDispatch(mActivity); 452 | mNfcAdapter.disableForegroundNdefPush(mActivity); 453 | } 454 | } 455 | mState = STATE_PAUSED; 456 | } 457 | 458 | /** 459 | * Call this method in your activity's onNewIntent(Intent) method body. 460 | * @return true if this call consumed the intent. 461 | */ 462 | public boolean onNewIntent(Activity activity, Intent intent) { 463 | // refresh mActivity 464 | mActivity = activity; 465 | if (mInterfaceMode == MODE_PASSTHROUGH) { 466 | return false; 467 | } 468 | 469 | // Check to see if the intent is ours to handle: 470 | if (!(NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction()) 471 | || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction()))) { 472 | return false; 473 | } 474 | 475 | new TagHandlerThread(mInterfaceMode, intent).start(); 476 | return true; 477 | } 478 | 479 | private class TagHandlerThread extends Thread { 480 | final int mmMode; 481 | final Intent mmIntent; 482 | 483 | TagHandlerThread(int mode, Intent intent) { 484 | mmMode = mode; 485 | mmIntent = intent; 486 | } 487 | 488 | @Override 489 | public void run() { 490 | // Check to see if we are writing to a tag 491 | if (mmMode == MODE_WRITE) { 492 | final Tag tag = mmIntent.getParcelableExtra(NfcAdapter.EXTRA_TAG); 493 | final NdefMessage ndef = mWriteMessage; 494 | if (tag != null && ndef != null) { 495 | OnTagWriteListener listener = mOnTagWriteListener; 496 | int status = writeTag(tag, ndef); 497 | if (listener != null) { 498 | listener.onTagWrite(status); 499 | } 500 | } 501 | return; 502 | } 503 | 504 | // In "exchange" mode. 505 | Parcelable[] rawMsgs = mmIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); 506 | if (rawMsgs == null || rawMsgs.length == 0) { 507 | return; 508 | } 509 | 510 | final NdefMessage[] ndefMessages = new NdefMessage[rawMsgs.length]; 511 | for (int i = 0; i < rawMsgs.length; i++) { 512 | ndefMessages[i] = (NdefMessage)rawMsgs[i]; 513 | } 514 | doHandleNdef(ndefMessages); 515 | } 516 | } 517 | 518 | public ConnectionHandoverManager getConnectionHandoverManager() { 519 | return mNdefExchangeManager; 520 | } 521 | 522 | /** 523 | * Puts the interface in mode {@link #MODE_PASSTHROUGH}. 524 | */ 525 | public void disable() { 526 | mInterfaceMode = MODE_PASSTHROUGH; 527 | mActivity.runOnUiThread(new Runnable() { 528 | @Override 529 | public void run() { 530 | synchronized(Nfc.this) { 531 | try { 532 | if (mState < STATE_RESUMING) { 533 | return; 534 | } 535 | mNfcAdapter.disableForegroundDispatch(mActivity); 536 | mNfcAdapter.disableForegroundNdefPush(mActivity); 537 | } catch (IllegalStateException e) { 538 | 539 | } 540 | } 541 | } 542 | }); 543 | } 544 | 545 | /** 546 | * Sets an ndef message to be read via android.npp protocol. 547 | * This method may be called off the main thread. 548 | */ 549 | private void enableNdefPush() { 550 | final NdefMessage ndef = mForegroundMessage; 551 | if (isConnectionHandoverEnabled()) { 552 | notifyRemoteNfcInteface(ndef); 553 | } 554 | 555 | if (!isNativeNfcAvailable()) { 556 | return; 557 | } 558 | 559 | if (ndef == null) { 560 | mActivity.runOnUiThread(new Runnable() { 561 | @Override 562 | public void run() { 563 | synchronized (Nfc.this) { 564 | if (mState < STATE_RESUMING) { 565 | return; 566 | } 567 | mNfcAdapter.disableForegroundNdefPush(mActivity); 568 | } 569 | } 570 | }); 571 | return; 572 | } else { 573 | mActivity.runOnUiThread(new Runnable() { 574 | @Override 575 | public void run() { 576 | synchronized (Nfc.this) { 577 | if (mState < STATE_RESUMING) { 578 | return; 579 | } 580 | mNfcAdapter.enableForegroundNdefPush(mActivity, ndef); 581 | } 582 | } 583 | }); 584 | } 585 | } 586 | 587 | private void notifyRemoteNfcInteface(NdefMessage ndef) { 588 | Intent intent = new Intent(ACTION_SET_NDEF); 589 | if (ndef != null) { 590 | NdefMessage[] ndefMessages = new NdefMessage[] { ndef }; 591 | intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, ndefMessages); 592 | } 593 | 594 | mActivity.sendBroadcast(intent); 595 | } 596 | 597 | /** 598 | * Requests any foreground NFC activity. This method must be called from 599 | * the main thread. 600 | */ 601 | private void installNfcHandler() { 602 | Intent activityIntent = new Intent(mActivity, mActivity.getClass()); 603 | activityIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); 604 | 605 | PendingIntent intent = PendingIntent.getActivity(mActivity, 0, 606 | activityIntent, PendingIntent.FLAG_CANCEL_CURRENT); 607 | mNfcAdapter.enableForegroundDispatch(mActivity, intent, mIntentFilters, mTechLists); 608 | } 609 | 610 | private BroadcastReceiver mHandoverReceiver = new BroadcastReceiver() { 611 | @Override 612 | public void onReceive(Context context, Intent intent) { 613 | new TagHandlerThread(mInterfaceMode, intent).start(); 614 | setResultCode(Activity.RESULT_CANCELED); 615 | } 616 | }; 617 | 618 | private void installNfcHandoverHandler() { 619 | IntentFilter handoverFilter = new IntentFilter(); 620 | handoverFilter.addAction(ACTION_HANDLE_NDEF); 621 | mActivity.registerReceiver(mHandoverReceiver, handoverFilter); 622 | } 623 | 624 | private void uninstallNfcHandoverHandler() { 625 | mActivity.unregisterReceiver(mHandoverReceiver); 626 | } 627 | 628 | /* 629 | * Credit: AOSP, via Android Tag application. 630 | * http://android.git.kernel.org/?p=platform/packages/apps/Tag.git;a=summary 631 | */ 632 | private int writeTag(Tag tag, NdefMessage message) { 633 | try { 634 | int size = message.toByteArray().length; 635 | Ndef ndef = Ndef.get(tag); 636 | if (ndef != null) { 637 | ndef.connect(); 638 | if (!ndef.isWritable()) { 639 | Log.w(TAG, "Tag is read-only."); 640 | return OnTagWriteListener.WRITE_ERROR_READ_ONLY; 641 | } 642 | if (ndef.getMaxSize() < size) { 643 | Log.d(TAG, "Tag capacity is " + ndef.getMaxSize() + " bytes, message is " + 644 | size + " bytes."); 645 | return OnTagWriteListener.WRITE_ERROR_CAPACITY; 646 | } 647 | 648 | ndef.writeNdefMessage(message); 649 | return OnTagWriteListener.WRITE_OK; 650 | } else { 651 | NdefFormatable format = NdefFormatable.get(tag); 652 | if (format != null) { 653 | try { 654 | format.connect(); 655 | format.format(message); 656 | return OnTagWriteListener.WRITE_OK; 657 | } catch (IOException e) { 658 | return OnTagWriteListener.WRITE_ERROR_IO_EXCEPTION; 659 | } 660 | } else { 661 | return OnTagWriteListener.WRITE_ERROR_BAD_FORMAT; 662 | } 663 | } 664 | } catch (Exception e) { 665 | Log.e(TAG, "Failed to write tag", e); 666 | } 667 | 668 | return OnTagWriteListener.WRITE_ERROR_IO_EXCEPTION; 669 | } 670 | 671 | private class EmptyNdefHandler implements NdefHandler, PrioritizedHandler { 672 | @Override 673 | public int handleNdef(NdefMessage[] ndefMessages) { 674 | return NdefFactory.isEmpty(ndefMessages[0]) ? NDEF_CONSUME : NDEF_PROPAGATE; 675 | } 676 | 677 | @Override 678 | public int getPriority() { 679 | return 0; 680 | } 681 | }; 682 | 683 | /** 684 | * @hide 685 | */ 686 | public Activity getContext() { 687 | return mActivity; 688 | } 689 | 690 | private void toast(final String text) { 691 | mActivity.runOnUiThread(new Runnable() { 692 | @Override 693 | public void run() { 694 | Toast.makeText(mActivity, text, Toast.LENGTH_LONG).show(); 695 | } 696 | }); 697 | } 698 | } -------------------------------------------------------------------------------- /src/main/java/mobisocial/nfc/addon/BluetoothConnector.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.addon; 19 | 20 | import java.io.IOException; 21 | import java.lang.reflect.Field; 22 | import java.lang.reflect.InvocationTargetException; 23 | import java.lang.reflect.Method; 24 | import java.util.Arrays; 25 | import java.util.Random; 26 | import java.util.UUID; 27 | 28 | import mobisocial.nfc.ConnectionHandover; 29 | import mobisocial.nfc.NdefFactory; 30 | import mobisocial.nfc.Nfc; 31 | import android.bluetooth.BluetoothAdapter; 32 | import android.bluetooth.BluetoothDevice; 33 | import android.bluetooth.BluetoothServerSocket; 34 | import android.bluetooth.BluetoothSocket; 35 | import android.content.Context; 36 | import android.net.Uri; 37 | import android.nfc.NdefMessage; 38 | import android.nfc.NdefRecord; 39 | import android.os.Build.VERSION; 40 | import android.os.Build.VERSION_CODES; 41 | import android.util.Log; 42 | 43 | import mobisocial.nfc.ConnectionHandoverManager; 44 | 45 | 46 | /** 47 | *

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 | *

    91 | *
  • Pairing when both phones are in the same activity 92 | *
  • Pairing when only one phone is in the activity 93 | *
  • Providing a download link if your application is not yet installed. 94 | *
95 | * 96 | *

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 | --------------------------------------------------------------------------------