From 381efc9ddbab94ba6d9c48efdea787c994d206ae Mon Sep 17 00:00:00 2001 From: Eric House Date: Sun, 29 Dec 2019 06:45:41 -0800 Subject: [PATCH] refactor to avoid ClassNotFound crash where SDK<19 --- .../eehouse/android/xw4/BoardDelegate.java | 2 +- .../eehouse/android/xw4/NFCCardService.java | 648 +----------------- .../org/eehouse/android/xw4/NFCUtils.java | 632 ++++++++++++++++- 3 files changed, 641 insertions(+), 641 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java index 994029ab0..025567323 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java @@ -72,7 +72,7 @@ import org.eehouse.android.xw4.jni.XwJNI.GamePtr; import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.TilePickAlert.TilePickState; -import org.eehouse.android.xw4.NFCCardService.Wrapper; +import org.eehouse.android.xw4.NFCUtils.Wrapper; public class BoardDelegate extends DelegateBase implements TransportProcs.TPMsgHandler, View.OnClickListener, diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java index e5c8f47b1..393ca9cba 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java @@ -21,204 +21,22 @@ package org.eehouse.android.xw4; import android.app.Activity; import android.content.Context; -import android.nfc.NfcAdapter; -import android.nfc.Tag; import android.nfc.cardemulation.HostApduService; -import android.nfc.tech.IsoDep; import android.os.Bundle; -import android.text.TextUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.IOException; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicInteger; +import org.eehouse.android.xw4.NFCUtils.HEX_STR; import org.eehouse.android.xw4.NFCUtils.MsgToken; -import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; public class NFCCardService extends HostApduService { private static final String TAG = NFCCardService.class.getSimpleName(); - private static final boolean USE_BIGINTEGER = true; private static final int LEN_OFFSET = 4; - private static final byte VERSION_1 = (byte)0x01; private int mMyDevID; - private static enum HEX_STR { - DEFAULT_CLA( "00" ) - , SELECT_INS( "A4" ) - , STATUS_FAILED( "6F00" ) - , CLA_NOT_SUPPORTED( "6E00" ) - , INS_NOT_SUPPORTED( "6D00" ) - , STATUS_SUCCESS( "9000" ) - , CMD_MSG_PART( "70FC" ) - ; - - private byte[] mBytes; - private HEX_STR( String hex ) { mBytes = Utils.hexStr2ba(hex); } - private byte[] asBA() { return mBytes; } - private boolean matchesFrom( byte[] src ) - { - return matchesFrom( src, 0 ); - } - private boolean matchesFrom( byte[] src, int offset ) - { - boolean result = offset + mBytes.length <= src.length; - for ( int ii = 0; result && ii < mBytes.length; ++ii ) { - result = src[offset + ii] == mBytes[ii]; - } - // Log.d( TAG, "%s.matchesFrom(%s) => %b", this, src, result ); - return result; - } - int length() { return asBA().length; } - } - - private static int sNextMsgID = 0; - private static synchronized int getNextMsgID() - { - return ++sNextMsgID; - } - - private static byte[] numTo( int num ) - { - byte[] result; - if ( USE_BIGINTEGER ) { - BigInteger bi = BigInteger.valueOf( num ); - byte[] bibytes = bi.toByteArray(); - result = new byte[1 + bibytes.length]; - result[0] = (byte)bibytes.length; - System.arraycopy( bibytes, 0, result, 1, bibytes.length ); - } else { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream( baos ); - try { - dos.writeInt( num ); - dos.flush(); - } catch ( IOException ioe ) { - Assert.assertFalse( BuildConfig.DEBUG ); - } - result = baos.toByteArray(); - } - // Log.d( TAG, "numTo(%d) => %s", num, DbgUtils.hexDump(result) ); - return result; - } - - private static int numFrom( ByteArrayInputStream bais ) throws IOException - { - int biLen = bais.read(); - // Log.d( TAG, "numFrom(): read biLen: %d", biLen ); - byte[] bytes = new byte[biLen]; - bais.read( bytes ); - BigInteger bi = new BigInteger( bytes ); - int result = bi.intValue(); - - // Log.d( TAG, "numFrom() => %d", result ); - return result; - } - - private static int numFrom( byte[] bytes, int start, int out[] ) - { - int result; - if ( USE_BIGINTEGER ) { - byte biLen = bytes[start]; - byte[] rest = Arrays.copyOfRange( bytes, start + 1, start + 1 + biLen ); - BigInteger bi = new BigInteger(rest); - out[0] = bi.intValue(); - result = biLen + 1; - } else { - ByteArrayInputStream bais = new ByteArrayInputStream( bytes, start, - bytes.length - start ); - DataInputStream dis = new DataInputStream( bais ); - try { - out[0] = dis.readInt(); - } catch ( IOException ioe ) { - Log.e( TAG, "from readInt(): %s", ioe.getMessage() ); - } - result = bais.available() - start; - } - return result; - } - - private static void testNumThing() - { - Log.d( TAG, "testNumThing() starting" ); - - int[] out = {0}; - for ( int ii = 1; ii > 0 && ii < Integer.MAX_VALUE; ii *= 2 ) { - byte[] tmp = numTo( ii ); - numFrom( tmp, 0, out ); - if ( ii != out[0] ) { - Log.d( TAG, "testNumThing(): %d failed; got %d", ii, out[0] ); - break; - } else { - Log.d( TAG, "testNumThing(): %d ok", ii ); - } - } - Log.d( TAG, "testNumThing() DONE" ); - } - - private static class QueueElem { - Context context; - byte[] msg; - QueueElem( Context pContext, byte[] pMsg ) { - context = pContext; - msg = pMsg; - } - } - - private static LinkedBlockingQueue sQueue = null; - - private synchronized static void addToMsgThread( Context context, byte[] msg ) - { - if ( 0 < msg.length ) { - QueueElem elem = new QueueElem( context, msg ); - if ( null == sQueue ) { - sQueue = new LinkedBlockingQueue<>(); - new Thread( new Runnable() { - @Override - public void run() { - Log.d( TAG, "addToMsgThread(): run starting" ); - for ( ; ; ) { - try { - QueueElem elem = sQueue.take(); - NFCUtils.receiveMsgs( elem.context, elem.msg ); - updateStatus( elem.context, true ); - } catch ( InterruptedException ie ) { - break; - } - } - Log.d( TAG, "addToMsgThread(): run exiting" ); - } - } ).start(); - } - sQueue.add( elem ); - // } else { - // // This is very common right now - // Log.d( TAG, "addToMsgThread(): dropping 0-length msg" ); - } - } - - private static void updateStatus( Context context, boolean in ) - { - if ( in ) { - ConnStatusHandler - .updateStatusIn( context, CommsConnType.COMMS_CONN_NFC, true ); - } else { - ConnStatusHandler - .updateStatusOut( context, CommsConnType.COMMS_CONN_NFC, true ); - } - } - // Remove this once we don't need logging to confirm stuff's loading @Override public void onCreate() @@ -241,9 +59,9 @@ public class NFCCardService extends HostApduService { if ( null != apdu ) { if ( HEX_STR.CMD_MSG_PART.matchesFrom( apdu ) ) { resStr = HEX_STR.STATUS_SUCCESS; - byte[] all = reassemble( this, apdu, HEX_STR.CMD_MSG_PART ); + byte[] all = NFCUtils.reassemble( this, apdu, HEX_STR.CMD_MSG_PART ); if ( null != all ) { - addToMsgThread( this, all ); + NFCUtils.addToMsgThread( this, all ); } } else { Log.d( TAG, "processCommandApdu(): aid case?" ); @@ -268,11 +86,11 @@ public class NFCCardService extends HostApduService { if ( BuildConfig.NFC_AID.equals( aidStr ) ) { byte minVersion = (byte)bais.read(); byte maxVersion = (byte)bais.read(); - if ( minVersion == VERSION_1 ) { - int devID = numFrom( bais ); + if ( minVersion == NFCUtils.VERSION_1 ) { + int devID = NFCUtils.numFrom( bais ); Log.d( TAG, "processCommandApdu(): read " + "remote devID: %d", devID ); - mGameID = numFrom( bais ); + mGameID = NFCUtils.numFrom( bais ); Log.d( TAG, "read gameID: %d", mGameID ); if ( 0 < bais.available() ) { Log.d( TAG, "processCommandApdu(): " @@ -301,11 +119,11 @@ public class NFCCardService extends HostApduService { baos.write( resStr.asBA() ); if ( HEX_STR.STATUS_SUCCESS == resStr ) { if ( isAidCase ) { - baos.write( VERSION_1 ); // min - baos.write( numTo( mMyDevID ) ); + baos.write( NFCUtils.VERSION_1 ); // min + baos.write( NFCUtils.numTo( mMyDevID ) ); } else { MsgToken token = NFCUtils.getMsgsFor( mGameID ); - byte[][] tmp = wrapMsg( token, Short.MAX_VALUE ); + byte[][] tmp = NFCUtils.wrapMsg( token, Short.MAX_VALUE ); Assert.assertTrue( 1 == tmp.length || !BuildConfig.DEBUG ); baos.write( tmp[0] ); } @@ -336,452 +154,4 @@ public class NFCCardService extends HostApduService { Log.d( TAG, "onDeactivated(reason=%s)", str ); } - - private static Map sSentTokens = new HashMap<>(); - private static void removeSentMsgs( Context context, int ack ) - { - MsgToken msgs = null; - if ( 0 != ack ) { - Log.d( TAG, "removeSentMsgs(msgID=%d)", ack ); - synchronized ( sSentTokens ) { - msgs = sSentTokens.remove( ack ); - Log.d( TAG, "removeSentMsgs(): removed %s, now have %s", msgs, keysFor() ); - } - updateStatus( context, false ); - } - if ( null != msgs ) { - msgs.removeSentMsgs(); - } - } - - private static void remember( int msgID, MsgToken msgs ) - { - if ( 0 != msgID ) { - Log.d( TAG, "remember(msgID=%d)", msgID ); - synchronized ( sSentTokens ) { - sSentTokens.put( msgID, msgs ); - Log.d( TAG, "remember(): now have %s", keysFor() ); - } - } - } - - private static String keysFor() - { - String result = ""; - if ( BuildConfig.DEBUG ) { - result = TextUtils.join( ",", sSentTokens.keySet() ); - } - return result; - } - - private static byte[][] sParts = null; - private static int sMsgID = 0; - private synchronized static byte[] reassemble( Context context, byte[] part, - HEX_STR cmd ) - { - return reassemble( context, part, cmd.length() ); - } - - private synchronized static byte[] reassemble( Context context, byte[] part, - int offset ) - { - part = Arrays.copyOfRange( part, offset, part.length ); - return reassemble( context, part ); - } - - private synchronized static byte[] reassemble( Context context, byte[] part ) - { - byte[] result = null; - try { - ByteArrayInputStream bais = new ByteArrayInputStream( part ); - - final int cur = bais.read(); - final int count = bais.read(); - if ( 0 == cur ) { - sMsgID = numFrom( bais ); - int ack = numFrom( bais ); - removeSentMsgs( context, ack ); - } - - boolean inSequence = true; - if ( sParts == null ) { - if ( 0 == cur ) { - sParts = new byte[count][]; - } else { - Log.e( TAG, "reassemble(): out-of-order message 1" ); - inSequence = false; - } - } else if ( cur >= count || count != sParts.length || null != sParts[cur] ) { - // result = HEX_STR.STATUS_FAILED; - inSequence = false; - Log.e( TAG, "reassemble(): out-of-order message 2" ); - } - - if ( !inSequence ) { - sParts = null; // so we can try again later - } else { - // write rest into array - byte[] rest = new byte[bais.available()]; - bais.read( rest, 0, rest.length ); - sParts[cur] = rest; - // Log.d( TAG, "addOrProcess(): added elem %d: %s", cur, DbgUtils.hexDump( rest ) ); - - // Done? Process!! - if ( cur + 1 == count ) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - for ( int ii = 0; ii < sParts.length; ++ii ) { - baos.write( sParts[ii] ); - } - sParts = null; - - result = baos.toByteArray(); - setLatestAck( sMsgID ); - if ( 0 != sMsgID ) { - Log.d( TAG, "reassemble(): done reassembling msgID=%d: %s", - sMsgID, DbgUtils.hexDump(result) ); - } - } - } - } catch ( IOException ioe ) { - Assert.assertFalse( BuildConfig.DEBUG ); - } - return result; - } - - private static AtomicInteger sLatestAck = new AtomicInteger(0); - private static int getLatestAck() - { - int result = sLatestAck.getAndSet(0); - if ( 0 != result ) { - Log.d( TAG, "getLatestAck() => %d", result ); - } - return result; - } - - private static void setLatestAck( int ack ) - { - if ( 0 != ack ) { - Log.e( TAG, "setLatestAck(%d)", ack ); - } - int oldVal = sLatestAck.getAndSet( ack ); - if ( 0 != oldVal ) { - Log.e( TAG, "setLatestAck(%d): dropping ack msgID %d", ack, oldVal ); - } - } - - private static final int HEADER_SIZE = 10; - private static byte[][] wrapMsg( MsgToken token, int maxLen ) - { - byte[] msg = token.getMsgs(); - final int length = null == msg ? 0 : msg.length; - final int msgID = (0 == length) ? 0 : getNextMsgID(); - if ( 0 < msgID ) { - Log.d( TAG, "wrapMsg(%s); msgID=%d", DbgUtils.hexDump( msg ), msgID ); - } - final int count = 1 + (length / (maxLen - HEADER_SIZE)); - byte[][] result = new byte[count][]; - try { - int offset = 0; - for ( int ii = 0; ii < count; ++ii ) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write( HEX_STR.CMD_MSG_PART.asBA() ); - baos.write( (byte)ii ); - baos.write( (byte)count ); - if ( 0 == ii ) { - baos.write( numTo( msgID ) ); - int latestAck = getLatestAck(); - baos.write( numTo( latestAck ) ); - } - Assert.assertTrue( HEADER_SIZE >= baos.toByteArray().length - || !BuildConfig.DEBUG ); - - int thisLen = Math.min( maxLen - HEADER_SIZE, length - offset ); - if ( 0 < thisLen ) { - // Log.d( TAG, "writing %d bytes starting from offset %d", - // thisLen, offset ); - baos.write( msg, offset, thisLen ); - offset += thisLen; - } - byte[] tmp = baos.toByteArray(); - // Log.d( TAG, "wrapMsg(): adding res[%d]: %s", ii, DbgUtils.hexDump(tmp) ); - result[ii] = tmp; - } - remember( msgID, token ); - } catch ( IOException ioe ) { - Assert.assertFalse( BuildConfig.DEBUG ); - } - return result; - } - - public static class Wrapper implements NfcAdapter.ReaderCallback, - NFCUtils.HaveDataListener { - private Activity mActivity; - private boolean mHaveData; - private Procs mProcs; - private NfcAdapter mAdapter; - private int mMinMS = 300; - private int mMaxMS = 500; - private boolean mConnected = false; - private int mMyDevID; - - public interface Procs { - void onReadingChange( boolean nowReading ); - } - - public static Wrapper init( Activity activity, Procs procs, int devID ) - { - Wrapper instance = null; - NfcAdapter adapter = NfcAdapter.getDefaultAdapter( activity ); - if ( null != adapter ) { - instance = new Wrapper( activity, adapter, procs, devID ); - } - Log.d( TAG, "Wrapper.init(devID=%d) => %s", devID, instance ); - return instance; - } - - static void setResumed( Wrapper instance, boolean resumed ) - { - if ( null != instance ) { - instance.setResumed( resumed ); - } - } - - static void setGameID( Wrapper instance, int gameID ) - { - if ( null != instance ) { - instance.setGameID( gameID ); - } - } - - private Wrapper( Activity activity, NfcAdapter adapter, Procs procs, - int devID ) - { - mActivity = activity; - mAdapter = adapter; - mProcs = procs; - mMyDevID = devID; - } - - private void setResumed( boolean resumed ) - { - if ( resumed ) { - startReadModeThread(); - } else { - stopReadModeThread(); - } - } - - @Override - public void onHaveDataChanged( boolean haveData ) - { - if ( mHaveData != haveData ) { - mHaveData = haveData; - Log.d( TAG, "onHaveDataChanged(): mHaveData now %b", mHaveData ); - interruptThread(); - } - } - - private boolean haveData() - { - boolean result = mHaveData; - // Log.d( TAG, "haveData() => %b", result ); - return result; - } - - private int mGameID; - private void setGameID( int gameID ) - { - Log.d( TAG, "setGameID(%d)", gameID ); - mGameID = gameID; - NFCUtils.setHaveDataListener( gameID, this ); - interruptThread(); - } - - private void interruptThread() - { - synchronized ( mThreadRef ) { - if ( null != mThreadRef[0] ) { - mThreadRef[0].interrupt(); - } - } - } - - @Override - public void onTagDiscovered( Tag tag ) - { - mConnected = true; - IsoDep isoDep = IsoDep.get( tag ); - try { - isoDep.connect(); - int maxLen = isoDep.getMaxTransceiveLength(); - Log.d( TAG, "onTagDiscovered() connected; max len: %d", maxLen ); - byte[] aidBytes = Utils.hexStr2ba( BuildConfig.NFC_AID ); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write( Utils.hexStr2ba( "00A40400" ) ); - baos.write( (byte)aidBytes.length ); - baos.write( aidBytes ); - baos.write( VERSION_1 ); // min - baos.write( VERSION_1 ); // max - baos.write( numTo( mMyDevID ) ); - baos.write( numTo( mGameID ) ); - byte[] msg = baos.toByteArray(); - Assert.assertTrue( msg.length < maxLen || !BuildConfig.DEBUG ); - byte[] response = isoDep.transceive( msg ); - - // The first reply from transceive() is special. If it starts - // with STATUS_SUCCESS then it also includes the version we'll - // be using to communicate, either what we sent over or - // something lower (for older code on the other side), and the - // remote's deviceID - if ( HEX_STR.STATUS_SUCCESS.matchesFrom( response ) ) { - int offset = HEX_STR.STATUS_SUCCESS.length(); - byte version = response[offset++]; - if ( version == VERSION_1 ) { - int[] out = {0}; - offset += numFrom( response, offset, out ); - Log.d( TAG, "onTagDiscovered(): read remote devID: %d", - out[0] ); - runMessageLoop( isoDep, maxLen ); - } else { - Log.e( TAG, "onTagDiscovered(): remote sent version %d, " - + "not %d; exiting", version, VERSION_1 ); - } - } - isoDep.close(); - } catch ( IOException ioe ) { - Log.e( TAG, "got ioe: " + ioe.getMessage() ); - } - - mConnected = false; - interruptThread(); // make sure we leave read mode! - Log.d( TAG, "onTagDiscovered() DONE" ); - } - - private void runMessageLoop( IsoDep isoDep, int maxLen ) throws IOException - { - outer: - for ( ; ; ) { - MsgToken token = NFCUtils.getMsgsFor( mGameID ); - // PENDING: no need for this Math.min thing once well tested - byte[][] toFit = wrapMsg( token, Math.min( 50, maxLen ) ); - for ( int ii = 0; ii < toFit.length; ++ii ) { - byte[] one = toFit[ii]; - Assert.assertTrue( one.length < maxLen || !BuildConfig.DEBUG ); - byte[] response = isoDep.transceive( one ); - if ( ! receiveAny( response ) ) { - break outer; - } - } - } - } - - private boolean receiveAny( byte[] response ) - { - boolean statusOK = HEX_STR.STATUS_SUCCESS.matchesFrom( response ); - if ( statusOK ) { - int offset = HEX_STR.STATUS_SUCCESS.length(); - if ( HEX_STR.CMD_MSG_PART.matchesFrom( response, offset ) ) { - byte[] all = reassemble( mActivity, response, - offset + HEX_STR.CMD_MSG_PART.length() ); - Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); - if ( null != all ) { - addToMsgThread( mActivity, all ); - - } - } - } - if ( !statusOK ) { - Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); - } - return statusOK; - } - - private class ReadModeThread extends Thread { - private boolean mShouldStop = false; - private boolean mInReadMode = false; - private final int mFlags = NfcAdapter.FLAG_READER_NFC_A - | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK; - - @Override - public void run() - { - Log.d( TAG, "ReadModeThread.run() starting" ); - Random random = new Random(); - - while ( !mShouldStop ) { - boolean wantReadMode = mConnected || !mInReadMode && haveData(); - if ( wantReadMode && !mInReadMode ) { - mAdapter.enableReaderMode( mActivity, Wrapper.this, mFlags, null ); - } else if ( mInReadMode && !wantReadMode ) { - mAdapter.disableReaderMode( mActivity ); - } - mInReadMode = wantReadMode; - Log.d( TAG, "run(): inReadMode now: %b", mInReadMode ); - - // Now sleep. If we aren't going to want to toggle read - // mode soon, sleep until interrupted by a state change, - // e.g. getting data or losing connection. - long intervalMS = Long.MAX_VALUE; - if ( (mInReadMode && !mConnected) || haveData() ) { - intervalMS = mMinMS + (Math.abs(random.nextInt()) - % (mMaxMS - mMinMS)); - } - try { - Thread.sleep( intervalMS ); - } catch ( InterruptedException ie ) { - Log.d( TAG, "run interrupted" ); - } - } - - // Kill read mode on the way out - if ( mInReadMode ) { - mAdapter.disableReaderMode( mActivity ); - mInReadMode = false; - } - - // Clear the reference only if it's me - synchronized ( mThreadRef ) { - if ( mThreadRef[0] == this ) { - mThreadRef[0] = null; - } - } - Log.d( TAG, "ReadModeThread.run() exiting" ); - } - - public void doStop() - { - mShouldStop = true; - interrupt(); - } - } - - private ReadModeThread[] mThreadRef = {null}; - private void startReadModeThread() - { - synchronized ( mThreadRef ) { - if ( null == mThreadRef[0] ) { - mThreadRef[0] = new ReadModeThread(); - mThreadRef[0].start(); - } - } - } - - private void stopReadModeThread() - { - ReadModeThread thread; - synchronized ( mThreadRef ) { - thread = mThreadRef[0]; - mThreadRef[0] = null; - } - - if ( null != thread ) { - thread.doStop(); - try { - thread.join(); - } catch ( InterruptedException ex ) { - Log.d( TAG, "stopReadModeThread(): %s", ex ); - } - } - } - } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java index 76a7bbd98..f2cebca5a 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java @@ -28,8 +28,11 @@ import android.content.Intent; import android.nfc.NfcAdapter; import android.nfc.NfcEvent; import android.nfc.NfcManager; +import android.nfc.Tag; +import android.nfc.tech.IsoDep; import android.os.Build; import android.os.Parcelable; +import android.text.TextUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -37,29 +40,37 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Random; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; import org.eehouse.android.xw4.MultiService.MultiEvent; +import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec; import org.eehouse.android.xw4.loc.LocUtils; public class NFCUtils { private static final String TAG = NFCUtils.class.getSimpleName(); + private static final boolean USE_BIGINTEGER = true; private static final String NFC_TO_SELF_ACTION = "org.eehouse.nfc_to_self"; private static final String NFC_TO_SELF_DATA = "nfc_data"; + static final byte VERSION_1 = (byte)0x01; + private static final byte MESSAGE = 0x01; private static final byte INVITE = 0x02; private static final byte REPLY = 0x03; private static final byte REPLY_NOGAME = 0x00; - private static boolean s_inSDK = 14 <= Build.VERSION.SDK_INT; + private static boolean s_inSDK = 19 <= Build.VERSION.SDK_INT; private static boolean[] s_nfcAvail; // Return array of two booleans, the first indicating whether the @@ -430,6 +441,625 @@ public class NFCUtils { } } + static enum HEX_STR { + DEFAULT_CLA( "00" ) + , SELECT_INS( "A4" ) + , STATUS_FAILED( "6F00" ) + , CLA_NOT_SUPPORTED( "6E00" ) + , INS_NOT_SUPPORTED( "6D00" ) + , STATUS_SUCCESS( "9000" ) + , CMD_MSG_PART( "70FC" ) + ; + + private byte[] mBytes; + private HEX_STR( String hex ) { mBytes = Utils.hexStr2ba(hex); } + byte[] asBA() { return mBytes; } + boolean matchesFrom( byte[] src ) + { + return matchesFrom( src, 0 ); + } + boolean matchesFrom( byte[] src, int offset ) + { + boolean result = offset + mBytes.length <= src.length; + for ( int ii = 0; result && ii < mBytes.length; ++ii ) { + result = src[offset + ii] == mBytes[ii]; + } + // Log.d( TAG, "%s.matchesFrom(%s) => %b", this, src, result ); + return result; + } + int length() { return asBA().length; } + } + + private static int sNextMsgID = 0; + private static synchronized int getNextMsgID() + { + return ++sNextMsgID; + } + + static byte[] numTo( int num ) + { + byte[] result; + if ( USE_BIGINTEGER ) { + BigInteger bi = BigInteger.valueOf( num ); + byte[] bibytes = bi.toByteArray(); + result = new byte[1 + bibytes.length]; + result[0] = (byte)bibytes.length; + System.arraycopy( bibytes, 0, result, 1, bibytes.length ); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream( baos ); + try { + dos.writeInt( num ); + dos.flush(); + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + result = baos.toByteArray(); + } + // Log.d( TAG, "numTo(%d) => %s", num, DbgUtils.hexDump(result) ); + return result; + } + + static int numFrom( ByteArrayInputStream bais ) throws IOException + { + int biLen = bais.read(); + // Log.d( TAG, "numFrom(): read biLen: %d", biLen ); + byte[] bytes = new byte[biLen]; + bais.read( bytes ); + BigInteger bi = new BigInteger( bytes ); + int result = bi.intValue(); + + // Log.d( TAG, "numFrom() => %d", result ); + return result; + } + + static int numFrom( byte[] bytes, int start, int out[] ) + { + int result; + if ( USE_BIGINTEGER ) { + byte biLen = bytes[start]; + byte[] rest = Arrays.copyOfRange( bytes, start + 1, start + 1 + biLen ); + BigInteger bi = new BigInteger(rest); + out[0] = bi.intValue(); + result = biLen + 1; + } else { + ByteArrayInputStream bais = new ByteArrayInputStream( bytes, start, + bytes.length - start ); + DataInputStream dis = new DataInputStream( bais ); + try { + out[0] = dis.readInt(); + } catch ( IOException ioe ) { + Log.e( TAG, "from readInt(): %s", ioe.getMessage() ); + } + result = bais.available() - start; + } + return result; + } + + // private static void testNumThing() + // { + // Log.d( TAG, "testNumThing() starting" ); + + // int[] out = {0}; + // for ( int ii = 1; ii > 0 && ii < Integer.MAX_VALUE; ii *= 2 ) { + // byte[] tmp = numTo( ii ); + // numFrom( tmp, 0, out ); + // if ( ii != out[0] ) { + // Log.d( TAG, "testNumThing(): %d failed; got %d", ii, out[0] ); + // break; + // } else { + // Log.d( TAG, "testNumThing(): %d ok", ii ); + // } + // } + // Log.d( TAG, "testNumThing() DONE" ); + // } + + private static AtomicInteger sLatestAck = new AtomicInteger(0); + static int getLatestAck() + { + int result = sLatestAck.getAndSet(0); + if ( 0 != result ) { + Log.d( TAG, "getLatestAck() => %d", result ); + } + return result; + } + + static void setLatestAck( int ack ) + { + if ( 0 != ack ) { + Log.e( TAG, "setLatestAck(%d)", ack ); + } + int oldVal = sLatestAck.getAndSet( ack ); + if ( 0 != oldVal ) { + Log.e( TAG, "setLatestAck(%d): dropping ack msgID %d", ack, oldVal ); + } + } + + private static void updateStatus( Context context, boolean in ) + { + if ( in ) { + ConnStatusHandler + .updateStatusIn( context, CommsConnType.COMMS_CONN_NFC, true ); + } else { + ConnStatusHandler + .updateStatusOut( context, CommsConnType.COMMS_CONN_NFC, true ); + } + } + + private static Map sSentTokens = new HashMap<>(); + private static void removeSentMsgs( Context context, int ack ) + { + MsgToken msgs = null; + if ( 0 != ack ) { + Log.d( TAG, "removeSentMsgs(msgID=%d)", ack ); + synchronized ( sSentTokens ) { + msgs = sSentTokens.remove( ack ); + Log.d( TAG, "removeSentMsgs(): removed %s, now have %s", msgs, keysFor() ); + } + updateStatus( context, false ); + } + if ( null != msgs ) { + msgs.removeSentMsgs(); + } + } + + private static void remember( int msgID, MsgToken msgs ) + { + if ( 0 != msgID ) { + Log.d( TAG, "remember(msgID=%d)", msgID ); + synchronized ( sSentTokens ) { + sSentTokens.put( msgID, msgs ); + Log.d( TAG, "remember(): now have %s", keysFor() ); + } + } + } + + private static String keysFor() + { + String result = ""; + if ( BuildConfig.DEBUG ) { + result = TextUtils.join( ",", sSentTokens.keySet() ); + } + return result; + } + + private static byte[][] sParts = null; + private static int sMsgID = 0; + synchronized static byte[] reassemble( Context context, byte[] part, + HEX_STR cmd ) + { + return reassemble( context, part, cmd.length() ); + } + + synchronized static byte[] reassemble( Context context, byte[] part, + int offset ) + { + part = Arrays.copyOfRange( part, offset, part.length ); + return reassemble( context, part ); + } + + synchronized static byte[] reassemble( Context context, byte[] part ) + { + byte[] result = null; + try { + ByteArrayInputStream bais = new ByteArrayInputStream( part ); + + final int cur = bais.read(); + final int count = bais.read(); + if ( 0 == cur ) { + sMsgID = NFCUtils.numFrom( bais ); + int ack = NFCUtils.numFrom( bais ); + removeSentMsgs( context, ack ); + } + + boolean inSequence = true; + if ( sParts == null ) { + if ( 0 == cur ) { + sParts = new byte[count][]; + } else { + Log.e( TAG, "reassemble(): out-of-order message 1" ); + inSequence = false; + } + } else if ( cur >= count || count != sParts.length || null != sParts[cur] ) { + // result = HEX_STR.STATUS_FAILED; + inSequence = false; + Log.e( TAG, "reassemble(): out-of-order message 2" ); + } + + if ( !inSequence ) { + sParts = null; // so we can try again later + } else { + // write rest into array + byte[] rest = new byte[bais.available()]; + bais.read( rest, 0, rest.length ); + sParts[cur] = rest; + // Log.d( TAG, "addOrProcess(): added elem %d: %s", cur, DbgUtils.hexDump( rest ) ); + + // Done? Process!! + if ( cur + 1 == count ) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for ( int ii = 0; ii < sParts.length; ++ii ) { + baos.write( sParts[ii] ); + } + sParts = null; + + result = baos.toByteArray(); + setLatestAck( sMsgID ); + if ( 0 != sMsgID ) { + Log.d( TAG, "reassemble(): done reassembling msgID=%d: %s", + sMsgID, DbgUtils.hexDump(result) ); + } + } + } + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + return result; + } + + private static final int HEADER_SIZE = 10; + static byte[][] wrapMsg( MsgToken token, int maxLen ) + { + byte[] msg = token.getMsgs(); + final int length = null == msg ? 0 : msg.length; + final int msgID = (0 == length) ? 0 : getNextMsgID(); + if ( 0 < msgID ) { + Log.d( TAG, "wrapMsg(%s); msgID=%d", DbgUtils.hexDump( msg ), msgID ); + } + final int count = 1 + (length / (maxLen - HEADER_SIZE)); + byte[][] result = new byte[count][]; + try { + int offset = 0; + for ( int ii = 0; ii < count; ++ii ) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write( HEX_STR.CMD_MSG_PART.asBA() ); + baos.write( (byte)ii ); + baos.write( (byte)count ); + if ( 0 == ii ) { + baos.write( numTo( msgID ) ); + int latestAck = getLatestAck(); + baos.write( numTo( latestAck ) ); + } + Assert.assertTrue( HEADER_SIZE >= baos.toByteArray().length + || !BuildConfig.DEBUG ); + + int thisLen = Math.min( maxLen - HEADER_SIZE, length - offset ); + if ( 0 < thisLen ) { + // Log.d( TAG, "writing %d bytes starting from offset %d", + // thisLen, offset ); + baos.write( msg, offset, thisLen ); + offset += thisLen; + } + byte[] tmp = baos.toByteArray(); + // Log.d( TAG, "wrapMsg(): adding res[%d]: %s", ii, DbgUtils.hexDump(tmp) ); + result[ii] = tmp; + } + remember( msgID, token ); + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + return result; + } + + private static class QueueElem { + Context context; + byte[] msg; + QueueElem( Context pContext, byte[] pMsg ) { + context = pContext; + msg = pMsg; + } + } + + private static LinkedBlockingQueue sQueue = null; + synchronized static void addToMsgThread( Context context, byte[] msg ) + { + if ( 0 < msg.length ) { + QueueElem elem = new QueueElem( context, msg ); + if ( null == sQueue ) { + sQueue = new LinkedBlockingQueue<>(); + new Thread( new Runnable() { + @Override + public void run() { + Log.d( TAG, "addToMsgThread(): run starting" ); + for ( ; ; ) { + try { + QueueElem elem = sQueue.take(); + NFCUtils.receiveMsgs( elem.context, elem.msg ); + updateStatus( elem.context, true ); + } catch ( InterruptedException ie ) { + break; + } + } + Log.d( TAG, "addToMsgThread(): run exiting" ); + } + } ).start(); + } + sQueue.add( elem ); + // } else { + // // This is very common right now + // Log.d( TAG, "addToMsgThread(): dropping 0-length msg" ); + } + } + + public static class Wrapper { + private Reader mReader; + + public interface Procs { + void onReadingChange( boolean nowReading ); + } + + private Wrapper( Activity activity, Procs procs, int devID ) + { + mReader = new Reader( activity, procs, devID ); + } + + public static Wrapper init( Activity activity, Procs procs, int devID ) + { + Wrapper instance = null; + + if ( nfcAvail( activity )[1] ) { + instance = new Wrapper( activity, procs, devID ); + } + Log.d( TAG, "Wrapper.init(devID=%d) => %s", devID, instance ); + return instance; + } + + static void setResumed( Wrapper instance, boolean resumed ) + { + if ( null != instance ) { + instance.mReader.setResumed( resumed ); + } + } + + static void setGameID( Wrapper instance, int gameID ) + { + if ( null != instance ) { + instance.mReader.setGameID( gameID ); + } + } + } + + private static class Reader implements NfcAdapter.ReaderCallback, + HaveDataListener { + private Activity mActivity; + private boolean mHaveData; + private Wrapper.Procs mProcs; + private NfcAdapter mAdapter; + private int mMinMS = 300; + private int mMaxMS = 500; + private boolean mConnected = false; + private int mMyDevID; + + private Reader( Activity activity, Wrapper.Procs procs, int devID ) + { + mActivity = activity; + mProcs = procs; + mMyDevID = devID; + mAdapter = NfcAdapter.getDefaultAdapter( activity ); + } + + private void setResumed( boolean resumed ) + { + if ( resumed ) { + startReadModeThread(); + } else { + stopReadModeThread(); + } + } + + @Override + public void onHaveDataChanged( boolean haveData ) + { + if ( mHaveData != haveData ) { + mHaveData = haveData; + Log.d( TAG, "onHaveDataChanged(): mHaveData now %b", mHaveData ); + interruptThread(); + } + } + + private boolean haveData() + { + boolean result = mHaveData; + // Log.d( TAG, "haveData() => %b", result ); + return result; + } + + private int mGameID; + private void setGameID( int gameID ) + { + Log.d( TAG, "setGameID(%d)", gameID ); + mGameID = gameID; + NFCUtils.setHaveDataListener( gameID, this ); + interruptThread(); + } + + private void interruptThread() + { + synchronized ( mThreadRef ) { + if ( null != mThreadRef[0] ) { + mThreadRef[0].interrupt(); + } + } + } + + @Override + public void onTagDiscovered( Tag tag ) + { + mConnected = true; + IsoDep isoDep = IsoDep.get( tag ); + try { + isoDep.connect(); + int maxLen = isoDep.getMaxTransceiveLength(); + Log.d( TAG, "onTagDiscovered() connected; max len: %d", maxLen ); + byte[] aidBytes = Utils.hexStr2ba( BuildConfig.NFC_AID ); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write( Utils.hexStr2ba( "00A40400" ) ); + baos.write( (byte)aidBytes.length ); + baos.write( aidBytes ); + baos.write( VERSION_1 ); // min + baos.write( VERSION_1 ); // max + baos.write( numTo( mMyDevID ) ); + baos.write( numTo( mGameID ) ); + byte[] msg = baos.toByteArray(); + Assert.assertTrue( msg.length < maxLen || !BuildConfig.DEBUG ); + byte[] response = isoDep.transceive( msg ); + + // The first reply from transceive() is special. If it starts + // with STATUS_SUCCESS then it also includes the version we'll + // be using to communicate, either what we sent over or + // something lower (for older code on the other side), and the + // remote's deviceID + if ( HEX_STR.STATUS_SUCCESS.matchesFrom( response ) ) { + int offset = HEX_STR.STATUS_SUCCESS.length(); + byte version = response[offset++]; + if ( version == VERSION_1 ) { + int[] out = {0}; + offset += numFrom( response, offset, out ); + Log.d( TAG, "onTagDiscovered(): read remote devID: %d", + out[0] ); + runMessageLoop( isoDep, maxLen ); + } else { + Log.e( TAG, "onTagDiscovered(): remote sent version %d, " + + "not %d; exiting", version, VERSION_1 ); + } + } + isoDep.close(); + } catch ( IOException ioe ) { + Log.e( TAG, "got ioe: " + ioe.getMessage() ); + } + + mConnected = false; + interruptThread(); // make sure we leave read mode! + Log.d( TAG, "onTagDiscovered() DONE" ); + } + + private void runMessageLoop( IsoDep isoDep, int maxLen ) throws IOException + { + outer: + for ( ; ; ) { + MsgToken token = NFCUtils.getMsgsFor( mGameID ); + // PENDING: no need for this Math.min thing once well tested + byte[][] toFit = wrapMsg( token, Math.min( 50, maxLen ) ); + for ( int ii = 0; ii < toFit.length; ++ii ) { + byte[] one = toFit[ii]; + Assert.assertTrue( one.length < maxLen || !BuildConfig.DEBUG ); + byte[] response = isoDep.transceive( one ); + if ( ! receiveAny( response ) ) { + break outer; + } + } + } + } + + private boolean receiveAny( byte[] response ) + { + boolean statusOK = HEX_STR.STATUS_SUCCESS.matchesFrom( response ); + if ( statusOK ) { + int offset = HEX_STR.STATUS_SUCCESS.length(); + if ( HEX_STR.CMD_MSG_PART.matchesFrom( response, offset ) ) { + byte[] all = reassemble( mActivity, response, + offset + HEX_STR.CMD_MSG_PART.length() ); + Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); + if ( null != all ) { + addToMsgThread( mActivity, all ); + } + } + } + if ( !statusOK ) { + Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); + } + return statusOK; + } + + private class ReadModeThread extends Thread { + private boolean mShouldStop = false; + private boolean mInReadMode = false; + private final int mFlags = NfcAdapter.FLAG_READER_NFC_A + | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK; + + @Override + public void run() + { + Log.d( TAG, "ReadModeThread.run() starting" ); + Random random = new Random(); + + while ( !mShouldStop ) { + boolean wantReadMode = mConnected || !mInReadMode && haveData(); + if ( wantReadMode && !mInReadMode ) { + mAdapter.enableReaderMode( mActivity, Reader.this, mFlags, null ); + } else if ( mInReadMode && !wantReadMode ) { + mAdapter.disableReaderMode( mActivity ); + } + mInReadMode = wantReadMode; + Log.d( TAG, "run(): inReadMode now: %b", mInReadMode ); + + // Now sleep. If we aren't going to want to toggle read + // mode soon, sleep until interrupted by a state change, + // e.g. getting data or losing connection. + long intervalMS = Long.MAX_VALUE; + if ( (mInReadMode && !mConnected) || haveData() ) { + intervalMS = mMinMS + (Math.abs(random.nextInt()) + % (mMaxMS - mMinMS)); + } + try { + Thread.sleep( intervalMS ); + } catch ( InterruptedException ie ) { + Log.d( TAG, "run interrupted" ); + } + } + + // Kill read mode on the way out + if ( mInReadMode ) { + mAdapter.disableReaderMode( mActivity ); + mInReadMode = false; + } + + // Clear the reference only if it's me + synchronized ( mThreadRef ) { + if ( mThreadRef[0] == this ) { + mThreadRef[0] = null; + } + } + Log.d( TAG, "ReadModeThread.run() exiting" ); + } + + public void doStop() + { + mShouldStop = true; + interrupt(); + } + } + + private ReadModeThread[] mThreadRef = {null}; + private void startReadModeThread() + { + synchronized ( mThreadRef ) { + if ( null == mThreadRef[0] ) { + mThreadRef[0] = new ReadModeThread(); + mThreadRef[0].start(); + } + } + } + + private void stopReadModeThread() + { + ReadModeThread thread; + synchronized ( mThreadRef ) { + thread = mThreadRef[0]; + mThreadRef[0] = null; + } + + if ( null != thread ) { + thread.doStop(); + try { + thread.join(); + } catch ( InterruptedException ex ) { + Log.d( TAG, "stopReadModeThread(): %s", ex ); + } + } + } + } + private static class NFCServiceHelper extends XWServiceHelper { private CommsAddrRec mAddr = new CommsAddrRec( CommsAddrRec.CommsConnType.COMMS_CONN_NFC );