mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2025-01-08 05:24:39 +01:00
refactor to avoid ClassNotFound crash where SDK<19
This commit is contained in:
parent
42da9b1ebf
commit
381efc9ddb
3 changed files with 641 additions and 641 deletions
|
@ -72,7 +72,7 @@ import org.eehouse.android.xw4.jni.XwJNI.GamePtr;
|
||||||
import org.eehouse.android.xw4.jni.XwJNI;
|
import org.eehouse.android.xw4.jni.XwJNI;
|
||||||
import org.eehouse.android.xw4.loc.LocUtils;
|
import org.eehouse.android.xw4.loc.LocUtils;
|
||||||
import org.eehouse.android.xw4.TilePickAlert.TilePickState;
|
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
|
public class BoardDelegate extends DelegateBase
|
||||||
implements TransportProcs.TPMsgHandler, View.OnClickListener,
|
implements TransportProcs.TPMsgHandler, View.OnClickListener,
|
||||||
|
|
|
@ -21,204 +21,22 @@ package org.eehouse.android.xw4;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.nfc.NfcAdapter;
|
|
||||||
import android.nfc.Tag;
|
|
||||||
import android.nfc.cardemulation.HostApduService;
|
import android.nfc.cardemulation.HostApduService;
|
||||||
import android.nfc.tech.IsoDep;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.DataInputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.IOException;
|
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.NFCUtils.MsgToken;
|
||||||
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
|
|
||||||
|
|
||||||
public class NFCCardService extends HostApduService {
|
public class NFCCardService extends HostApduService {
|
||||||
private static final String TAG = NFCCardService.class.getSimpleName();
|
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 int LEN_OFFSET = 4;
|
||||||
private static final byte VERSION_1 = (byte)0x01;
|
|
||||||
|
|
||||||
private int mMyDevID;
|
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<QueueElem> 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
|
// Remove this once we don't need logging to confirm stuff's loading
|
||||||
@Override
|
@Override
|
||||||
public void onCreate()
|
public void onCreate()
|
||||||
|
@ -241,9 +59,9 @@ public class NFCCardService extends HostApduService {
|
||||||
if ( null != apdu ) {
|
if ( null != apdu ) {
|
||||||
if ( HEX_STR.CMD_MSG_PART.matchesFrom( apdu ) ) {
|
if ( HEX_STR.CMD_MSG_PART.matchesFrom( apdu ) ) {
|
||||||
resStr = HEX_STR.STATUS_SUCCESS;
|
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 ) {
|
if ( null != all ) {
|
||||||
addToMsgThread( this, all );
|
NFCUtils.addToMsgThread( this, all );
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d( TAG, "processCommandApdu(): aid case?" );
|
Log.d( TAG, "processCommandApdu(): aid case?" );
|
||||||
|
@ -268,11 +86,11 @@ public class NFCCardService extends HostApduService {
|
||||||
if ( BuildConfig.NFC_AID.equals( aidStr ) ) {
|
if ( BuildConfig.NFC_AID.equals( aidStr ) ) {
|
||||||
byte minVersion = (byte)bais.read();
|
byte minVersion = (byte)bais.read();
|
||||||
byte maxVersion = (byte)bais.read();
|
byte maxVersion = (byte)bais.read();
|
||||||
if ( minVersion == VERSION_1 ) {
|
if ( minVersion == NFCUtils.VERSION_1 ) {
|
||||||
int devID = numFrom( bais );
|
int devID = NFCUtils.numFrom( bais );
|
||||||
Log.d( TAG, "processCommandApdu(): read "
|
Log.d( TAG, "processCommandApdu(): read "
|
||||||
+ "remote devID: %d", devID );
|
+ "remote devID: %d", devID );
|
||||||
mGameID = numFrom( bais );
|
mGameID = NFCUtils.numFrom( bais );
|
||||||
Log.d( TAG, "read gameID: %d", mGameID );
|
Log.d( TAG, "read gameID: %d", mGameID );
|
||||||
if ( 0 < bais.available() ) {
|
if ( 0 < bais.available() ) {
|
||||||
Log.d( TAG, "processCommandApdu(): "
|
Log.d( TAG, "processCommandApdu(): "
|
||||||
|
@ -301,11 +119,11 @@ public class NFCCardService extends HostApduService {
|
||||||
baos.write( resStr.asBA() );
|
baos.write( resStr.asBA() );
|
||||||
if ( HEX_STR.STATUS_SUCCESS == resStr ) {
|
if ( HEX_STR.STATUS_SUCCESS == resStr ) {
|
||||||
if ( isAidCase ) {
|
if ( isAidCase ) {
|
||||||
baos.write( VERSION_1 ); // min
|
baos.write( NFCUtils.VERSION_1 ); // min
|
||||||
baos.write( numTo( mMyDevID ) );
|
baos.write( NFCUtils.numTo( mMyDevID ) );
|
||||||
} else {
|
} else {
|
||||||
MsgToken token = NFCUtils.getMsgsFor( mGameID );
|
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 );
|
Assert.assertTrue( 1 == tmp.length || !BuildConfig.DEBUG );
|
||||||
baos.write( tmp[0] );
|
baos.write( tmp[0] );
|
||||||
}
|
}
|
||||||
|
@ -336,452 +154,4 @@ public class NFCCardService extends HostApduService {
|
||||||
|
|
||||||
Log.d( TAG, "onDeactivated(reason=%s)", str );
|
Log.d( TAG, "onDeactivated(reason=%s)", str );
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<Integer, MsgToken> 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 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,11 @@ import android.content.Intent;
|
||||||
import android.nfc.NfcAdapter;
|
import android.nfc.NfcAdapter;
|
||||||
import android.nfc.NfcEvent;
|
import android.nfc.NfcEvent;
|
||||||
import android.nfc.NfcManager;
|
import android.nfc.NfcManager;
|
||||||
|
import android.nfc.Tag;
|
||||||
|
import android.nfc.tech.IsoDep;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -37,29 +40,37 @@ import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.MultiService.MultiEvent;
|
||||||
|
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
|
||||||
import org.eehouse.android.xw4.jni.CommsAddrRec;
|
import org.eehouse.android.xw4.jni.CommsAddrRec;
|
||||||
import org.eehouse.android.xw4.loc.LocUtils;
|
import org.eehouse.android.xw4.loc.LocUtils;
|
||||||
|
|
||||||
public class NFCUtils {
|
public class NFCUtils {
|
||||||
private static final String TAG = NFCUtils.class.getSimpleName();
|
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_ACTION = "org.eehouse.nfc_to_self";
|
||||||
private static final String NFC_TO_SELF_DATA = "nfc_data";
|
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 MESSAGE = 0x01;
|
||||||
private static final byte INVITE = 0x02;
|
private static final byte INVITE = 0x02;
|
||||||
private static final byte REPLY = 0x03;
|
private static final byte REPLY = 0x03;
|
||||||
|
|
||||||
private static final byte REPLY_NOGAME = 0x00;
|
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;
|
private static boolean[] s_nfcAvail;
|
||||||
|
|
||||||
// Return array of two booleans, the first indicating whether the
|
// 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<Integer, MsgToken> 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<QueueElem> 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 static class NFCServiceHelper extends XWServiceHelper {
|
||||||
private CommsAddrRec mAddr
|
private CommsAddrRec mAddr
|
||||||
= new CommsAddrRec( CommsAddrRec.CommsConnType.COMMS_CONN_NFC );
|
= new CommsAddrRec( CommsAddrRec.CommsConnType.COMMS_CONN_NFC );
|
||||||
|
|
Loading…
Reference in a new issue