package com.chariotsolutions.nfc.plugin;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter.MalformedMimeTypeException;
import android.net.Uri;
import android.nfc.*;
import android.nfc.tech.Ndef;
import android.nfc.tech.NdefFormatable;
import android.os.Parcelable;
import android.util.Log;
import org.apache.cordova.*;
import org.apache.cordova.api.*; // for Cordova 2.9
// import org.apache.cordova.CallbackContext;
// import org.apache.cordova.CordovaPlugin;
// import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
public class NfcPlugin extends CordovaPlugin implements NfcAdapter.OnNdefPushCompleteCallback {
private static final String REGISTER_MIME_TYPE = "registerMimeType";
private static final String REGISTER_NDEF = "registerNdef";
private static final String REGISTER_NDEF_FORMATABLE = "registerNdefFormatable";
private static final String REGISTER_DEFAULT_TAG = "registerTag";
private static final String WRITE_TAG = "writeTag";
private static final String ERASE_TAG = "eraseTag";
private static final String SHARE_TAG = "shareTag";
private static final String UNSHARE_TAG = "unshareTag";
private static final String HANDOVER = "handover"; // Android Beam
private static final String STOP_HANDOVER = "stopHandover";
private static final String INIT = "init";
private static final String NDEF = "ndef";
private static final String NDEF_MIME = "ndef-mime";
private static final String NDEF_FORMATABLE = "ndef-formatable";
private static final String TAG_DEFAULT = "tag";
private static final String STATUS_NFC_OK = "NFC_OK";
private static final String STATUS_NO_NFC = "NO_NFC";
private static final String STATUS_NFC_DISABLED = "NFC_DISABLED";
private static final String STATUS_NDEF_PUSH_DISABLED = "NDEF_PUSH_DISABLED";
private static final String TAG = "NfcPlugin";
private final List<IntentFilter> intentFilters = new ArrayList<IntentFilter>();
private final ArrayList<String[]> techLists = new ArrayList<String[]>();
private NdefMessage p2pMessage = null;
private PendingIntent pendingIntent = null;
private Intent savedIntent = null;
private CallbackContext shareTagCallback;
private CallbackContext handoverCallback;
@Override
public boolean execute(String action, JSONArray data, CallbackContext callbackContext) throws JSONException {
Log.d(TAG, "execute " + action);
if (!getNfcStatus().equals(STATUS_NFC_OK)) {
callbackContext.error(getNfcStatus());
return true; // short circuit
}
createPendingIntent();
if (action.equalsIgnoreCase(REGISTER_MIME_TYPE)) {
registerMimeType(data, callbackContext);
} else if (action.equalsIgnoreCase(REGISTER_NDEF)) {
registerNdef(callbackContext);
} else if (action.equalsIgnoreCase(REGISTER_NDEF_FORMATABLE)) {
registerNdefFormatable(callbackContext);
} else if (action.equals(REGISTER_DEFAULT_TAG)) {
registerDefaultTag(callbackContext);
} else if (action.equalsIgnoreCase(WRITE_TAG)) {
writeTag(data, callbackContext);
} else if (action.equalsIgnoreCase(ERASE_TAG)) {
eraseTag(callbackContext);
} else if (action.equalsIgnoreCase(SHARE_TAG)) {
shareTag(data, callbackContext);
} else if (action.equalsIgnoreCase(UNSHARE_TAG)) {
unshareTag(callbackContext);
} else if (action.equalsIgnoreCase(HANDOVER)) {
handover(data, callbackContext);
} else if (action.equalsIgnoreCase(STOP_HANDOVER)) {
stopHandover(callbackContext);
} else if (action.equalsIgnoreCase(INIT)) {
init(callbackContext);
} else {
// invalid action
return false;
}
return true;
}
private String getNfcStatus() {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
if (nfcAdapter == null) {
return STATUS_NO_NFC;
} else if (!nfcAdapter.isEnabled()) {
return STATUS_NFC_DISABLED;
} else {
return STATUS_NFC_OK;
}
}
private void registerDefaultTag(CallbackContext callbackContext) {
addTagFilter();
callbackContext.success();
}
private void registerNdefFormatable(CallbackContext callbackContext) {
addTechList(new String[]{NdefFormatable.class.getName()});
callbackContext.success();
}
private void registerNdef(CallbackContext callbackContext) {
addTechList(new String[]{Ndef.class.getName()});
callbackContext.success();
}
private void unshareTag(CallbackContext callbackContext) {
p2pMessage = null;
stopNdefPush();
shareTagCallback = null;
callbackContext.success();
}
private void init(CallbackContext callbackContext) {
Log.d(TAG, "Enabling plugin " + getIntent());
startNfc();
if (!recycledIntent()) {
parseMessage();
}
callbackContext.success();
}
private void registerMimeType(JSONArray data, CallbackContext callbackContext) throws JSONException {
String mimeType = "";
try {
mimeType = data.getString(0);
intentFilters.add(createIntentFilter(mimeType));
callbackContext.success();
} catch (MalformedMimeTypeException e) {
callbackContext.error("Invalid MIME Type " + mimeType);
}
}
// Cheating and writing an empty record. We may actually be able to erase some tag types.
private void eraseTag(CallbackContext callbackContext) throws JSONException {
Tag tag = savedIntent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
NdefRecord[] records = {
new NdefRecord(NdefRecord.TNF_EMPTY, new byte[0], new byte[0], new byte[0])
};
writeNdefMessage(new NdefMessage(records), tag, callbackContext);
}
private void writeTag(JSONArray data, CallbackContext callbackContext) throws JSONException {
if (getIntent() == null) { // TODO remove this and handle LostTag
callbackContext.error("Failed to write tag, received null intent");
}
Tag tag = savedIntent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
NdefRecord[] records = Util.jsonToNdefRecords(data.getString(0));
writeNdefMessage(new NdefMessage(records), tag, callbackContext);
}
private void writeNdefMessage(final NdefMessage message, final Tag tag, final CallbackContext callbackContext) {
cordova.getThreadPool().execute(new Runnable() {
@Override
public void run() {
try {
Ndef ndef = Ndef.get(tag);
if (ndef != null) {
ndef.connect();
if (ndef.isWritable()) {
int size = message.toByteArray().length;
if (ndef.getMaxSize() < size) {
callbackContext.error("Tag capacity is " + ndef.getMaxSize() +
" bytes, message is " + size + " bytes.");
} else {
ndef.writeNdefMessage(message);
callbackContext.success();
}
} else {
callbackContext.error("Tag is read only");
}
ndef.close();
} else {
NdefFormatable formatable = NdefFormatable.get(tag);
if (formatable != null) {
formatable.connect();
formatable.format(message);
callbackContext.success();
formatable.close();
} else {
callbackContext.error("Tag doesn't support NDEF");
}
}
} catch (FormatException e) {
callbackContext.error(e.getMessage());
} catch (TagLostException e) {
callbackContext.error(e.getMessage());
} catch (IOException e) {
callbackContext.error(e.getMessage());
}
}
});
}
private void shareTag(JSONArray data, CallbackContext callbackContext) throws JSONException {
NdefRecord[] records = Util.jsonToNdefRecords(data.getString(0));
this.p2pMessage = new NdefMessage(records);
startNdefPush(callbackContext);
}
// setBeamPushUris
// Every Uri you provide must have either scheme 'file' or scheme 'content'.
// Note that this takes priority over setNdefPush
//
// See http://developer.android.com/reference/android/nfc/NfcAdapter.html#setBeamPushUris(android.net.Uri[],%20android.app.Activity)
private void handover(JSONArray data, CallbackContext callbackContext) throws JSONException {
Uri[] uri = new Uri[data.length()];
for (int i = 0; i < data.length(); i++) {
uri[i] = Uri.parse(data.getString(i));
}
startNdefBeam(callbackContext, uri);
}
private void stopHandover(CallbackContext callbackContext) throws JSONException {
stopNdefBeam();
handoverCallback = null;
callbackContext.success();
}
private void createPendingIntent() {
if (pendingIntent == null) {
Activity activity = getActivity();
Intent intent = new Intent(activity, activity.getClass());
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
pendingIntent = PendingIntent.getActivity(activity, 0, intent, 0);
}
}
private void addTechList(String[] list) {
this.addTechFilter();
this.addToTechList(list);
}
private void addTechFilter() {
intentFilters.add(new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED));
}
private void addTagFilter() {
intentFilters.add(new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED));
}
private void startNfc() {
createPendingIntent(); // onResume can call startNfc before execute
getActivity().runOnUiThread(new Runnable() {
public void run() {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
if (nfcAdapter != null && !getActivity().isFinishing()) {
nfcAdapter.enableForegroundDispatch(getActivity(), getPendingIntent(), getIntentFilters(), getTechLists());
if (p2pMessage != null) {
nfcAdapter.setNdefPushMessage(p2pMessage, getActivity());
}
}
}
});
}
private void stopNfc() {
Log.d(TAG, "stopNfc");
getActivity().runOnUiThread(new Runnable() {
public void run() {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
if (nfcAdapter != null) {
nfcAdapter.disableForegroundDispatch(getActivity());
}
}
});
}
private void startNdefBeam(final CallbackContext callbackContext, final Uri[] uris) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
if (nfcAdapter == null) {
callbackContext.error(STATUS_NO_NFC);
} else if (!nfcAdapter.isNdefPushEnabled()) {
callbackContext.error(STATUS_NDEF_PUSH_DISABLED);
} else {
nfcAdapter.setOnNdefPushCompleteCallback(NfcPlugin.this, getActivity());
try {
nfcAdapter.setBeamPushUris(uris, getActivity());
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
handoverCallback = callbackContext;
callbackContext.sendPluginResult(result);
} catch (IllegalArgumentException e) {
callbackContext.error(e.getMessage());
}
}
}
});
}
private void startNdefPush(final CallbackContext callbackContext) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
if (nfcAdapter == null) {
callbackContext.error(STATUS_NO_NFC);
} else if (!nfcAdapter.isNdefPushEnabled()) {
callbackContext.error(STATUS_NDEF_PUSH_DISABLED);
} else {
nfcAdapter.setNdefPushMessage(p2pMessage, getActivity());
nfcAdapter.setOnNdefPushCompleteCallback(NfcPlugin.this, getActivity());
PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
result.setKeepCallback(true);
shareTagCallback = callbackContext;
callbackContext.sendPluginResult(result);
}
}
});
}
private void stopNdefPush() {
getActivity().runOnUiThread(new Runnable() {
public void run() {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
if (nfcAdapter != null) {
nfcAdapter.setNdefPushMessage(null, getActivity());
}
}
});
}
private void stopNdefBeam() {
getActivity().runOnUiThread(new Runnable() {
public void run() {
NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(getActivity());
if (nfcAdapter != null) {
nfcAdapter.setBeamPushUris(null, getActivity());
}
}
});
}
private void addToTechList(String[] techs) {
techLists.add(techs);
}
private IntentFilter createIntentFilter(String mimeType) throws MalformedMimeTypeException {
IntentFilter intentFilter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
intentFilter.addDataType(mimeType);
return intentFilter;
}
private PendingIntent getPendingIntent() {
return pendingIntent;
}
private IntentFilter[] getIntentFilters() {
return intentFilters.toArray(new IntentFilter[intentFilters.size()]);
}
private String[][] getTechLists() {
//noinspection ToArrayCallWithZeroLengthArrayArgument
return techLists.toArray(new String[0][0]);
}
void parseMessage() {
cordova.getThreadPool().execute(new Runnable() {
@Override
public void run() {
Log.d(TAG, "parseMessage " + getIntent());
Intent intent = getIntent();
String action = intent.getAction();
Log.d(TAG, "action " + action);
if (action == null) {
return;
}
Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
Parcelable[] messages = intent.getParcelableArrayExtra((NfcAdapter.EXTRA_NDEF_MESSAGES));
if (action.equals(NfcAdapter.ACTION_NDEF_DISCOVERED)) {
Ndef ndef = Ndef.get(tag);
fireNdefEvent(NDEF_MIME, ndef, messages);
} else if (action.equals(NfcAdapter.ACTION_TECH_DISCOVERED)) {
for (String tagTech : tag.getTechList()) {
Log.d(TAG, tagTech);
if (tagTech.equals(NdefFormatable.class.getName())) {
fireNdefFormatableEvent(tag);
} else if (tagTech.equals(Ndef.class.getName())) { //
Ndef ndef = Ndef.get(tag);
fireNdefEvent(NDEF, ndef, messages);
}
}
}
if (action.equals(NfcAdapter.ACTION_TAG_DISCOVERED)) {
fireTagEvent(tag);
}
setIntent(new Intent());
}
});
}
private void fireNdefEvent(String type, Ndef ndef, Parcelable[] messages) {
JSONObject jsonObject = buildNdefJSON(ndef, messages);
String tag = jsonObject.toString();
String command = MessageFormat.format(javaScriptEventTemplate, type, tag);
Log.v(TAG, command);
this.webView.sendJavascript(command);
}
private void fireNdefFormatableEvent (Tag tag) {
String command = MessageFormat.format(javaScriptEventTemplate, NDEF_FORMATABLE, Util.tagToJSON(tag));
Log.v(TAG, command);
this.webView.sendJavascript(command);
}
private void fireTagEvent (Tag tag) {
String command = MessageFormat.format(javaScriptEventTemplate, TAG_DEFAULT, Util.tagToJSON(tag));
Log.v(TAG, command);
this.webView.sendJavascript(command);
}
JSONObject buildNdefJSON(Ndef ndef, Parcelable[] messages) {
JSONObject json = Util.ndefToJSON(ndef);
// ndef is null for peer-to-peer
// ndef and messages are null for ndef format-able
if (ndef == null && messages != null) {
try {
if (messages.length > 0) {
NdefMessage message = (NdefMessage) messages[0];
json.put("ndefMessage", Util.messageToJSON(message));
// guessing type, would prefer a more definitive way to determine type
json.put("type", "NDEF Push Protocol");
}
if (messages.length > 1) {
Log.wtf(TAG, "Expected one ndefMessage but found " + messages.length);
}
} catch (JSONException e) {
// shouldn't happen
Log.e(Util.TAG, "Failed to convert ndefMessage into json", e);
}
}
return json;
}
private boolean recycledIntent() { // TODO this is a kludge, find real solution
int flags = getIntent().getFlags();
if ((flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) {
Log.i(TAG, "Launched from history, killing recycled intent");
setIntent(new Intent());
return true;
}
return false;
}
@Override
public void onPause(boolean multitasking) {
Log.d(TAG, "onPause " + getIntent());
super.onPause(multitasking);
if (multitasking) {
// nfc can't run in background
stopNfc();
}
}
@Override
public void onResume(boolean multitasking) {
Log.d(TAG, "onResume " + getIntent());
super.onResume(multitasking);
startNfc();
}
@Override
public void onNewIntent(Intent intent) {
Log.d(TAG, "onNewIntent " + intent);
super.onNewIntent(intent);
setIntent(intent);
savedIntent = intent;
parseMessage();
}
private Activity getActivity() {
return this.cordova.getActivity();
}
private Intent getIntent() {
return getActivity().getIntent();
}
private void setIntent(Intent intent) {
getActivity().setIntent(intent);
}
String javaScriptEventTemplate =
"var e = document.createEvent(''Events'');\n" +
"e.initEvent(''{0}'');\n" +
"e.tag = {1};\n" +
"document.dispatchEvent(e);";
@Override
public void onNdefPushComplete(NfcEvent event) {
// handover (beam) take precedence over share tag (ndef push)
if (handoverCallback != null) {
PluginResult result = new PluginResult(PluginResult.Status.OK, "Beamed Message to Peer");
result.setKeepCallback(true);
handoverCallback.sendPluginResult(result);
} else if (shareTagCallback != null) {
PluginResult result = new PluginResult(PluginResult.Status.OK, "Shared Message with Peer");
result.setKeepCallback(true);
shareTagCallback.sendPluginResult(result);
}
}
}