public class

Nfc

extends Object
/*
 * 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();
			}
		});
	}
}