/*
* Copyright (C) 2011 Stanford University MobiSocial Lab
* http://mobisocial.stanford.edu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package mobisocial.nfc;
import java.io.IOException;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import mobisocial.ndefexchange.NdefExchangeContract;
import mobisocial.ndefexchange.NdefExchangeManager;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.nfc.tech.NdefFormatable;
import android.os.Build;
import android.os.Parcelable;
import android.util.Log;
import android.widget.Toast;
/**
* <p>This class acts as an abstraction layer for Android's Nfc stack.
* The goals of this project are to:
* <ul>
* <li>Provide compatibility for Nfc across platforms.
* <li>Degrade gracefully on platforms and devices lacking Nfc support.
* <li>Extend the capabilities of Nfc by following Connection Handover Requests.
* <li>Simplify the Nfc experience for developers.
* </ul>
* </p>
*
* <p>
* The Nfc class must be run from a foregrounded activity. It requires
* a few lifecycle events be triggered during an activity's runtime:
* </p>
* <pre class="prettyprint">
*
* class MyActivity extends Activity {
*
* public void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
* mNfc = new Nfc(this);
* mNfc.onCreate(this);
* }
*
* public void onResume() {
* super.onResume();
* mNfc.onResume(this);
* // your activity's onResume code
* }
*
* public void onPause() {
* super.onPause();
* mNfc.onPause(this);
* // your activity's onPause code
* }
*
* public void onNewIntent(Intent intent) {
* if (mNfc.onNewIntent(this, intent)) {
* return;
* }
* // your activity's onNewIntent code
* }
* }
* </pre>
* <p>
* Your application must hold the {@code android.permission.NFC}
* permission to use this class. However, this class will degrade gracefully
* on devices lacking Nfc capabilities.
* </p>
* <p>
* The Nfc interface can be in one of three modes: {@link #MODE_WRITE}, for writing
* to a passive NFC tag, and {@link #MODE_EXCHANGE}, in which the interface can
* read data from passive tags and exchange data with another active Nfc device, or
* {@link #MODE_PASSTHROUGH} which disables this interface.
* <ul>
* <li>{@link #share(NdefMessage)} and similar, to share messages with other Nfc devices.
* <li>{@link #addNdefHandler(NdefHandler)}, for acting on received messages.
* <li>{@link #enableTagWriteMode(NdefMessage)}, to write to physical Nfc tags.
* </ul>
* </p>
*/
public class Nfc {
private static final String TAG = "easynfc";
private Activity mActivity;
private NfcAdapter mNfcAdapter;
private final IntentFilter[] mIntentFilters;
private final String[][] mTechLists;
private NdefMessage mForegroundMessage = null;
private NdefMessage mWriteMessage = null;
private final Map<Integer, Set<NdefHandler>> mNdefHandlers = new TreeMap<Integer, Set<NdefHandler>>();
private boolean mHandoverEnabled = true;
private OnTagWriteListener mOnTagWriteListener = null;
private final ConnectionHandoverManager mNdefExchangeManager;
private int mState = STATE_PAUSED;
private int mInterfaceMode = MODE_EXCHANGE;
private static final int STATE_PAUSED = 0;
private static final int STATE_PAUSING = 1;
private static final int STATE_RESUMING = 2;
private static final int STATE_RESUMED = 3;
/**
* A broadcasted intent used to set an NDEF message for use in a Connection
* Handover, for devices that do not have an active NFC radio.
*/
protected static final String ACTION_SET_NDEF = "mobisocial.intent.action.SET_NDEF";
/**
* The action of an ordered broadcast intent for applications to handle a
* received NDEF messages. Such intents are broadcast from connection
* handover services. This library sets the result code to
* {@code Activity.RESULT_CANCELED}, indicating the foreground application has
* consumed the intent.
*/
protected static final String ACTION_HANDLE_NDEF = "mobisocial.intent.action.HANDLE_NDEF";
/**
* Nfc interface mode in which Nfc interaction is disabled for this class.
*/
public static final int MODE_PASSTHROUGH = 0;
/**
* Nfc interface mode for reading data from a passive tag
* or exchanging information with another active device.
* See {@link #addNdefHandler(NdefHandler)} and
* {@link #share(NdefMessage)} for handling the actual data.
*/
public static final int MODE_EXCHANGE = 1;
/**
* Nfc interface mode for writing data to a passive tag.
*/
public static final int MODE_WRITE = 2;
public Nfc(Activity activity, IntentFilter[] intentFilters, String[][] techLists) {
mActivity = activity;
mIntentFilters = intentFilters;
mTechLists = techLists;
if (Build.VERSION.SDK_INT >= NfcWrapper.SDK_NDEF_DEFINED && PackageManager.PERMISSION_GRANTED !=
mActivity.checkCallingOrSelfPermission("android.permission.NFC")) {
throw new SecurityException("Application must hold android.permission.NFC to use libhotpotato.");
}
if (PackageManager.PERMISSION_GRANTED !=
mActivity.checkCallingOrSelfPermission("android.permission.BLUETOOTH")) {
Log.w(TAG, "No android.permission.BLUETOOTH permission; bluetooth handover not supported.");
}
if (PackageManager.PERMISSION_GRANTED !=
mActivity.checkCallingOrSelfPermission("android.permission.INTERNET")) {
Log.w(TAG, "No android.permission.INTERNET permission; internet handover not supported.");
}
if (NfcWrapper.getInstance() != null) {
mNfcAdapter = NfcWrapper.getInstance().getAdapter(mActivity);
}
if (mNfcAdapter == null) {
Log.i(TAG, "Nfc implementation not available.");
}
mNdefExchangeManager = new NdefExchangeManager(new NdefExchangeContract() {
@Override
public int handleNdef(NdefMessage[] ndef) {
doHandleNdef(ndef);
return NDEF_CONSUME;
}
@Override
public NdefMessage getForegroundNdefMessage() {
return mForegroundMessage;
}
});
addNdefHandler(mNdefExchangeManager);
addNdefHandler(new EmptyNdefHandler());
}
public Nfc(Activity activity) {
this(activity, null, null);
}
/**
* Returns true if this device has a native NFC implementation.
*/
public boolean isNativeNfcAvailable() {
return mNfcAdapter != null;
}
/**
* Removes any message from being shared with an interested reader.
*/
public void clearSharing() {
mForegroundMessage = null;
synchronized(this) {
if (mState == STATE_RESUMED) {
enableNdefPush();
}
}
}
/**
* Makes an ndef message available to any interested reader.
* @see NdefFactory
*/
public void share(NdefMessage ndefMessage) {
mForegroundMessage = ndefMessage;
synchronized(this) {
if (mState == STATE_RESUMED) {
enableNdefPush();
}
}
}
public void share(Uri uri) {
mForegroundMessage = NdefFactory.fromUri(uri);
synchronized(this) {
if (mState == STATE_RESUMED) {
enableNdefPush();
}
}
}
public void share(URL url) {
mForegroundMessage = NdefFactory.fromUrl(url);
synchronized(this) {
if (mState == STATE_RESUMED) {
enableNdefPush();
}
}
}
public void share (String mimeType, byte[] data) {
mForegroundMessage = NdefFactory.fromMime(mimeType, data);
synchronized(this) {
if (mState == STATE_RESUMED) {
enableNdefPush();
}
}
}
/**
* Sets a callback to call when an Nfc tag is written.
*/
public void setOnTagWriteListener(OnTagWriteListener listener) {
mOnTagWriteListener = listener;
}
/**
* Disallows connection handover requests.
*/
public void disableConnectionHandover() {
mHandoverEnabled = false;
}
/**
* Enables support for connection handover requests.
*/
public void enableConnectionHandover() {
mHandoverEnabled = true;
}
/**
* Returns true if connection handovers are currently supported.
*/
public boolean isConnectionHandoverEnabled() {
return mHandoverEnabled;
}
/**
* Sets a callback to call when an Nfc tag is written.
*/
public void addNdefHandler(NdefHandler handler) {
if (handler instanceof PrioritizedHandler) {
addNdefHandler(((PrioritizedHandler)handler).getPriority(), handler);
} else {
addNdefHandler(PrioritizedHandler.DEFAULT_PRIORITY, handler);
}
}
public synchronized void addNdefHandler(Integer priority, NdefHandler handler) {
if (!mNdefHandlers.containsKey(priority)) {
mNdefHandlers.put(priority, new HashSet<NdefHandler>());
}
Set<NdefHandler> handlers = mNdefHandlers.get(priority);
handlers.add(handler);
}
public synchronized void clearNdefHandlers() {
mNdefHandlers.clear();
}
private synchronized void doHandleNdef(NdefMessage[] ndefMessages) {
Iterator<Integer> bins = mNdefHandlers.keySet().iterator();
while (bins.hasNext()) {
Integer priority = bins.next();
Iterator<NdefHandler> handlers = mNdefHandlers.get(priority).iterator();
while (handlers.hasNext()) {
NdefHandler handler = handlers.next();
if (handler.handleNdef(ndefMessages) == NdefHandler.NDEF_CONSUME) {
return;
}
}
}
}
/**
* Interface definition for a callback called after an attempt to write
* an Nfc tag.
*/
public interface OnTagWriteListener {
public static final int WRITE_OK = 0;
public static final int WRITE_ERROR_READ_ONLY = 1;
public static final int WRITE_ERROR_CAPACITY = 2;
public static final int WRITE_ERROR_BAD_FORMAT = 3;
public static final int WRITE_ERROR_IO_EXCEPTION = 4;
/**
* Callback issued after an attempt to write an NFC tag.
* This method is executed off the main thread, so be careful when
* updating UI elements as a result of this callback.
*/
public void onTagWrite(int status);
}
/**
* Puts the interface in mode {@link #MODE_WRITE}.
* @param ndef The NdefMessage to write to a discovered tag.
* @throws NullPointerException if ndef is null.
*/
public void enableTagWriteMode(NdefMessage ndef) {
if (mNfcAdapter == null) {
return;
}
if (ndef == null) {
throw new NullPointerException("Cannot write null NDEF message.");
}
mWriteMessage = ndef;
mInterfaceMode = MODE_WRITE;
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mState == STATE_RESUMED && mInterfaceMode == MODE_WRITE) {
installNfcHandler();
}
}
});
}
/**
* Puts the interface in mode {@link #MODE_EXCHANGE},
* the default mode of operation for this Nfc interface.
*/
public void enableExchangeMode() {
mInterfaceMode = MODE_EXCHANGE;
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (mState == STATE_RESUMED) {
installNfcHandler();
enableNdefPush();
}
}
});
}
public void onCreate(Activity activity) {
onNewIntent(activity, activity.getIntent());
}
/**
* Call this method in your Activity's onResume() method body.
*/
public void onResume(Activity activity) {
// refresh mActivity
mActivity = activity;
mState = STATE_RESUMING;
if (isConnectionHandoverEnabled()) {
installNfcHandoverHandler();
enableNdefPush();
}
if (mNfcAdapter != null) {
synchronized(this) {
if (mInterfaceMode != MODE_PASSTHROUGH) {
installNfcHandler();
if (mInterfaceMode == MODE_EXCHANGE) {
enableNdefPush();
}
}
}
}
mState = STATE_RESUMED;
}
/**
* Call this method in your Activity's onPause() method body.
*/
public void onPause(Activity activity) {
// refresh mActivity
mActivity = activity;
mState = STATE_PAUSING;
if (isConnectionHandoverEnabled()) {
uninstallNfcHandoverHandler();
notifyRemoteNfcInteface(null);
}
if (mNfcAdapter != null) {
synchronized(this) {
mNfcAdapter.disableForegroundDispatch(mActivity);
mNfcAdapter.disableForegroundNdefPush(mActivity);
}
}
mState = STATE_PAUSED;
}
/**
* Call this method in your activity's onNewIntent(Intent) method body.
* @return true if this call consumed the intent.
*/
public boolean onNewIntent(Activity activity, Intent intent) {
// refresh mActivity
mActivity = activity;
if (mInterfaceMode == MODE_PASSTHROUGH) {
return false;
}
// Check to see if the intent is ours to handle:
if (!(NfcAdapter.ACTION_TAG_DISCOVERED.equals(intent.getAction())
|| NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction()))) {
return false;
}
new TagHandlerThread(mInterfaceMode, intent).start();
return true;
}
private class TagHandlerThread extends Thread {
final int mmMode;
final Intent mmIntent;
TagHandlerThread(int mode, Intent intent) {
mmMode = mode;
mmIntent = intent;
}
@Override
public void run() {
// Check to see if we are writing to a tag
if (mmMode == MODE_WRITE) {
final Tag tag = mmIntent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
final NdefMessage ndef = mWriteMessage;
if (tag != null && ndef != null) {
OnTagWriteListener listener = mOnTagWriteListener;
int status = writeTag(tag, ndef);
if (listener != null) {
listener.onTagWrite(status);
}
}
return;
}
// In "exchange" mode.
Parcelable[] rawMsgs = mmIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
if (rawMsgs == null || rawMsgs.length == 0) {
return;
}
final NdefMessage[] ndefMessages = new NdefMessage[rawMsgs.length];
for (int i = 0; i < rawMsgs.length; i++) {
ndefMessages[i] = (NdefMessage)rawMsgs[i];
}
doHandleNdef(ndefMessages);
}
}
public ConnectionHandoverManager getConnectionHandoverManager() {
return mNdefExchangeManager;
}
/**
* Puts the interface in mode {@link #MODE_PASSTHROUGH}.
*/
public void disable() {
mInterfaceMode = MODE_PASSTHROUGH;
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
synchronized(Nfc.this) {
try {
if (mState < STATE_RESUMING) {
return;
}
mNfcAdapter.disableForegroundDispatch(mActivity);
mNfcAdapter.disableForegroundNdefPush(mActivity);
} catch (IllegalStateException e) {
}
}
}
});
}
/**
* Sets an ndef message to be read via android.npp protocol.
* This method may be called off the main thread.
*/
private void enableNdefPush() {
final NdefMessage ndef = mForegroundMessage;
if (isConnectionHandoverEnabled()) {
notifyRemoteNfcInteface(ndef);
}
if (!isNativeNfcAvailable()) {
return;
}
if (ndef == null) {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
synchronized (Nfc.this) {
if (mState < STATE_RESUMING) {
return;
}
mNfcAdapter.disableForegroundNdefPush(mActivity);
}
}
});
return;
} else {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
synchronized (Nfc.this) {
if (mState < STATE_RESUMING) {
return;
}
mNfcAdapter.enableForegroundNdefPush(mActivity, ndef);
}
}
});
}
}
private void notifyRemoteNfcInteface(NdefMessage ndef) {
Intent intent = new Intent(ACTION_SET_NDEF);
if (ndef != null) {
NdefMessage[] ndefMessages = new NdefMessage[] { ndef };
intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, ndefMessages);
}
mActivity.sendBroadcast(intent);
}
/**
* Requests any foreground NFC activity. This method must be called from
* the main thread.
*/
private void installNfcHandler() {
Intent activityIntent = new Intent(mActivity, mActivity.getClass());
activityIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent intent = PendingIntent.getActivity(mActivity, 0,
activityIntent, PendingIntent.FLAG_CANCEL_CURRENT);
mNfcAdapter.enableForegroundDispatch(mActivity, intent, mIntentFilters, mTechLists);
}
private BroadcastReceiver mHandoverReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
new TagHandlerThread(mInterfaceMode, intent).start();
setResultCode(Activity.RESULT_CANCELED);
}
};
private void installNfcHandoverHandler() {
IntentFilter handoverFilter = new IntentFilter();
handoverFilter.addAction(ACTION_HANDLE_NDEF);
mActivity.registerReceiver(mHandoverReceiver, handoverFilter);
}
private void uninstallNfcHandoverHandler() {
mActivity.unregisterReceiver(mHandoverReceiver);
}
/*
* Credit: AOSP, via Android Tag application.
* http://android.git.kernel.org/?p=platform/packages/apps/Tag.git;a=summary
*/
private int writeTag(Tag tag, NdefMessage message) {
try {
int size = message.toByteArray().length;
Ndef ndef = Ndef.get(tag);
if (ndef != null) {
ndef.connect();
if (!ndef.isWritable()) {
Log.w(TAG, "Tag is read-only.");
return OnTagWriteListener.WRITE_ERROR_READ_ONLY;
}
if (ndef.getMaxSize() < size) {
Log.d(TAG, "Tag capacity is " + ndef.getMaxSize() + " bytes, message is " +
size + " bytes.");
return OnTagWriteListener.WRITE_ERROR_CAPACITY;
}
ndef.writeNdefMessage(message);
return OnTagWriteListener.WRITE_OK;
} else {
NdefFormatable format = NdefFormatable.get(tag);
if (format != null) {
try {
format.connect();
format.format(message);
return OnTagWriteListener.WRITE_OK;
} catch (IOException e) {
return OnTagWriteListener.WRITE_ERROR_IO_EXCEPTION;
}
} else {
return OnTagWriteListener.WRITE_ERROR_BAD_FORMAT;
}
}
} catch (Exception e) {
Log.e(TAG, "Failed to write tag", e);
}
return OnTagWriteListener.WRITE_ERROR_IO_EXCEPTION;
}
private class EmptyNdefHandler implements NdefHandler, PrioritizedHandler {
@Override
public int handleNdef(NdefMessage[] ndefMessages) {
return NdefFactory.isEmpty(ndefMessages[0]) ? NDEF_CONSUME : NDEF_PROPAGATE;
}
@Override
public int getPriority() {
return 0;
}
};
/**
* @hide
*/
public Activity getContext() {
return mActivity;
}
private void toast(final String text) {
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mActivity, text, Toast.LENGTH_LONG).show();
}
});
}
}