import android.annotation.TargetApi; | |
import android.nfc.Tag; | |
import android.nfc.tech.IsoDep; | |
import android.os.Build; | |
import android.os.Handler; | |
import android.os.HandlerThread; | |
import android.os.Looper; | |
import android.support.annotation.NonNull; | |
import android.support.annotation.Nullable; | |
import android.util.Log; | |
import java.lang.ref.WeakReference; | |
import java.lang.reflect.InvocationTargetException; | |
import java.lang.reflect.Method; | |
/** | |
* <p>The purpose of this class is to keep the android system from | |
* sending keep-alive commands to NFC tags.</p> | |
* <p>This is necessary on some of the most common devices because | |
* their implementation of keep-alive isn't according to the NFC | |
* specification. The result of this is that a keep-alive command | |
* can abort an authentication process that utilizes a | |
* challenge-response mechanism, which Mifare DESFire does.</p> | |
* <p>A common usage pattern will be to do some NFC communication, | |
* call {@link #holdConnection(IsoDep)}, communicate with a webservice, | |
* call {@link #stopHoldingConnection()}, and do some more NFC | |
* communication.</p> | |
*/ | |
@TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) | |
public class NFCWatchdogRefresher { | |
private static final String TAG = NFCWatchdogRefresher.class.getSimpleName(); | |
private static final int TECHNOLOGY_ISO_DEP = 3; | |
@Nullable | |
private static HandlerThread sHandlerThread; | |
@Nullable | |
private static Handler sHandler; | |
@Nullable | |
private static WatchdogRefresher sRefresher; | |
private static volatile boolean sIsRunning = false; | |
/** | |
* <p>Should be called as soon as possible after the last NFC communication.</p> | |
* <p>If this method is called multiple times without any calls to | |
* {@link #stopHoldingConnection()}, each subsequent call will automatically | |
* cancel the previous one.</p> | |
*/ | |
public static void holdConnection(IsoDep isoDep) { | |
Log.v(TAG, "holdConnection()"); | |
if (sHandlerThread != null || sHandler != null || sRefresher != null) { | |
Log.d(TAG, "holdConnection(): Existing background thread found, stopping!"); | |
stopHoldingConnection(); | |
} | |
sHandlerThread = new HandlerThread("NFCWatchdogRefresherThread"); | |
try { | |
sHandlerThread.start(); | |
} catch (IllegalThreadStateException e) { | |
Log.d(TAG, "holdConnection(): Failed starting background thread!", e); | |
} | |
Looper looper = sHandlerThread.getLooper(); | |
if (looper != null) { | |
sHandler = new Handler(looper); | |
} else { | |
Log.d(TAG, "holdConnection(): No looper on background thread!"); | |
sHandlerThread.quit(); | |
sHandler = new Handler(); | |
} | |
sIsRunning = true; | |
sRefresher = new WatchdogRefresher(sHandler, isoDep); | |
sHandler.post(sRefresher); | |
} | |
/** | |
* Should be called before NFC communication is made if | |
* {@link #holdConnection(IsoDep)} has been called since | |
* the last communication. | |
*/ | |
public static void stopHoldingConnection() { | |
Log.v(TAG, "stopHoldingConnection()"); | |
sIsRunning = false; | |
if (sHandler != null) { | |
if (sRefresher != null) { | |
sHandler.removeCallbacks(sRefresher); | |
} | |
sHandler.removeCallbacksAndMessages(null); | |
sHandler = null; | |
} | |
if (sRefresher != null) { | |
sRefresher = null; | |
} | |
if (sHandlerThread != null) { | |
sHandlerThread.quit(); | |
sHandlerThread = null; | |
} | |
} | |
/** | |
* Runnable that uses reflection to keep the NFC watchdog from | |
* reaching its timeout and sending a keep-alive communication. | |
* <p/> | |
* This works by telling the TagService to connect, if the tag | |
* is already connected, it will return the success status and reset | |
* the timeout. | |
* <p/> | |
* The default timeout is 125ms, this runnable will call connect | |
* every {@link #INTERVAL} (100ms). This runnable will self-terminate | |
* after {@link #RUNTIME_MAX} has been reached (30 seconds) to avoid | |
* accidentally leaking the thread. | |
*/ | |
// Only supported in API 19 | |
@SuppressWarnings("TryWithIdenticalCatches") | |
private static class WatchdogRefresher implements Runnable { | |
/** | |
* Delay between each refresh in millis | |
*/ | |
private static final int INTERVAL = 100; | |
/** | |
* Used to ensure that this runnable self-stops after 30 seconds | |
* if not stopped externally. | |
*/ | |
private static final int RUNTIME_MAX = 30 * 1000; | |
private final WeakReference<Handler> mHandler; | |
private final WeakReference<IsoDep> mIsoDep; | |
private int mCurrentRuntime; | |
private WatchdogRefresher(@NonNull Handler handler, @NonNull IsoDep isoDep) { | |
mHandler = new WeakReference<>(handler); | |
mIsoDep = new WeakReference<>(isoDep); | |
mCurrentRuntime = 0; | |
} | |
@Override | |
public void run() { | |
Tag tag = getTag(); | |
if (tag != null) { | |
try { | |
Method getTagService = Tag.class.getMethod("getTagService"); | |
Object tagService = getTagService.invoke(tag); | |
Method getServiceHandle = Tag.class.getMethod("getServiceHandle"); | |
Object serviceHandle = getServiceHandle.invoke(tag); | |
Method connect = tagService.getClass().getMethod("connect", int.class, int.class); | |
/** | |
* int result = tag.getTagService().connect(tag.getServiceHandle(), selectedTechnology); | |
*/ | |
Object result = connect.invoke(tagService, serviceHandle, TECHNOLOGY_ISO_DEP); | |
Handler handler = getHandler(); | |
if (result != null && result.equals(0) && handler != null && sIsRunning && mCurrentRuntime < RUNTIME_MAX) { | |
handler.postDelayed(this, INTERVAL); | |
mCurrentRuntime += INTERVAL; | |
Log.v(TAG, "Told NFC Watchdog to wait"); | |
} else { | |
Log.d(TAG, "result: " + result); | |
} | |
} catch (InvocationTargetException e) { | |
Log.d(TAG, "WatchdogRefresher.run()", e); | |
} catch (NoSuchMethodException e) { | |
Log.d(TAG, "WatchdogRefresher.run()", e); | |
} catch (IllegalAccessException e) { | |
Log.d(TAG, "WatchdogRefresher.run()", e); | |
} | |
} | |
} | |
@Nullable | |
private Handler getHandler() { | |
return mHandler.get(); | |
} | |
@Nullable | |
private Tag getTag() { | |
IsoDep isoDep = mIsoDep.get(); | |
if (isoDep != null) { | |
return isoDep.getTag(); | |
} | |
return null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment