support sending invites and moves via NFC

Use low-level NFC, a combination of emulated card and reader mode, to
work around Google's removal of "beaming" support from Android 10. App
emulates a card by declaring support in its AndroidManifest. When a game
is open that has data to send, it goes periodically into read mode. If
two devices are touched while one is in read mode and the other isn't,
they handshake and open a connection that should last until they're
separated. The devices loop, sending messages back and forth with or
without data (as available.)
This commit is contained in:
Eric House 2019-12-23 08:45:55 -08:00
parent ccaa3c67fc
commit 0153928bcd
24 changed files with 1332 additions and 242 deletions

View file

@ -4,6 +4,10 @@ def VERSION_NAME = '4.4.150'
def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY") def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY")
def BUILD_INFO_NAME = "build-info.txt" def BUILD_INFO_NAME = "build-info.txt"
// AID must start with F (first 4 bits) and be at from 5 to 16 bytes long
def NFC_AID_XW4 = "FC8FF510B360"
def NFC_AID_XW4d = "FDDA0A3EB5E5"
boolean forFDroid = hasProperty('forFDroid') boolean forFDroid = hasProperty('forFDroid')
// Get the git revision we're using. Since fdroid modifies files as // Get the git revision we're using. Since fdroid modifies files as
@ -35,6 +39,7 @@ android {
// default changes and .travis.yml can be kept in sync // default changes and .travis.yml can be kept in sync
buildToolsVersion '27.0.3' buildToolsVersion '27.0.3'
defaultConfig { defaultConfig {
// HostApduService requires 19. But is it a problem?
minSdkVersion 14 minSdkVersion 14
targetSdkVersion 28 // must match ../build.gradle targetSdkVersion 28 // must match ../build.gradle
versionCode VERSION_CODE_BASE versionCode VERSION_CODE_BASE
@ -88,6 +93,8 @@ android {
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false"
buildConfigField "String", "VARIANT_NAME", "\"Google Play Store\"" buildConfigField "String", "VARIANT_NAME", "\"Google Play Store\""
buildConfigField "int", "VARIANT_CODE", "1" buildConfigField "int", "VARIANT_CODE", "1"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4"
} }
xw4fdroid { xw4fdroid {
@ -101,10 +108,12 @@ android {
buildConfigField "String", "VARIANT_NAME", "\"F-Droid\"" buildConfigField "String", "VARIANT_NAME", "\"F-Droid\""
buildConfigField "int", "VARIANT_CODE", "2" buildConfigField "int", "VARIANT_CODE", "2"
buildConfigField "boolean", "FOR_FDROID", "true" buildConfigField "boolean", "FOR_FDROID", "true"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4"
} }
xw4d { xw4d {
dimension "variant" dimension "variant"
buildConfigField "String", "DB_NAME", "\"xwddb\""; buildConfigField "String", "DB_NAME", "\"xwddb\""
applicationId "org.eehouse.android.xw4dbg" applicationId "org.eehouse.android.xw4dbg"
manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ] manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ]
resValue "string", "app_name", "CrossDbg" resValue "string", "app_name", "CrossDbg"
@ -115,11 +124,13 @@ android {
buildConfigField "int", "VARIANT_CODE", "3" buildConfigField "int", "VARIANT_CODE", "3"
buildConfigField "boolean", "REPORT_LOCKS", "true" buildConfigField "boolean", "REPORT_LOCKS", "true"
buildConfigField "boolean", "MOVE_VIA_NFC", "true" buildConfigField "boolean", "MOVE_VIA_NFC", "true"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4d"
} }
xw4dNoSMS { xw4dNoSMS {
dimension "variant" dimension "variant"
buildConfigField "String", "DB_NAME", "\"xwddb\""; buildConfigField "String", "DB_NAME", "\"xwddb\""
applicationId "org.eehouse.android.xw4dbg" applicationId "org.eehouse.android.xw4dbg"
manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ] manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ]
resValue "string", "app_name", "CrossDbg" resValue "string", "app_name", "CrossDbg"
@ -130,6 +141,8 @@ android {
buildConfigField "int", "VARIANT_CODE", "4" buildConfigField "int", "VARIANT_CODE", "4"
buildConfigField "boolean", "REPORT_LOCKS", "true" buildConfigField "boolean", "REPORT_LOCKS", "true"
buildConfigField "boolean", "MOVE_VIA_NFC", "true" buildConfigField "boolean", "MOVE_VIA_NFC", "true"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4d"
} }
xw4SMS { xw4SMS {
@ -142,6 +155,8 @@ android {
buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false"
buildConfigField "String", "VARIANT_NAME", "\"FOSS\"" buildConfigField "String", "VARIANT_NAME", "\"FOSS\""
buildConfigField "int", "VARIANT_CODE", "5" buildConfigField "int", "VARIANT_CODE", "5"
buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\""
resValue "string", "nfc_aid", "$NFC_AID_XW4"
} }
// WARNING: "all" breaks things. Seems to be a keyword. Need // WARNING: "all" breaks things. Seems to be a keyword. Need
@ -390,8 +405,8 @@ task makeBuildAssets() {
out += "date: ${date}\n" out += "date: ${date}\n"
// I want the variant, but that's harder. Here's a quick hack from SO. // I want the variant, but that's harder. Here's a quick hack from SO.
String target = gradle.startParameter.taskNames[-1] // String target = gradle.startParameter.taskNames[0]
out += "target: ${target}\n" // out += "target: ${target}\n"
String diff = "git diff".execute().text.trim() String diff = "git diff".execute().text.trim()
if (diff) { if (diff) {

View file

@ -32,10 +32,10 @@
<uses-feature android:name="android.hardware.telephony" <uses-feature android:name="android.hardware.telephony"
android:required = "false" android:required = "false"
/> />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
<application android:icon="@drawable/icon48x48" <application android:icon="@drawable/icon48x48"
android:label="@string/app_name" android:label="@string/app_name"
@ -62,12 +62,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="@string/xwords_nfc_mime" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -213,5 +207,14 @@
</intent-filter> </intent-filter>
</service> </service>
<service android:name="NFCCardService" android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
</intent-filter>
<meta-data android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice"/>
</service>
</application> </application>
</manifest> </manifest>

View file

@ -39,7 +39,7 @@
<h3>Next up</h3> <h3>Next up</h3>
<ul> <ul>
<li>Look for a workaround to allow NFC on Android 10</li> <li>Fix email invitations</li>
<li>Support duplicate-style play (popular in France)</li> <li>Support duplicate-style play (popular in France)</li>
<li>Improve play-by-data-sms workaround <li>Improve play-by-data-sms workaround
using <a href="https://github.com/eehouse/nbsproxy">NBSProxy</a></li> using <a href="https://github.com/eehouse/nbsproxy">NBSProxy</a></li>

View file

@ -771,14 +771,14 @@ public class BTService extends XWJIService {
{ {
Context context = XWApp.getContext(); Context context = XWApp.getContext();
ConnStatusHandler ConnStatusHandler
.updateStatusOut( context, null, CommsConnType.COMMS_CONN_BT, success ); .updateStatusOut( context, CommsConnType.COMMS_CONN_BT, success );
} }
private static void updateStatusIn( boolean success ) private static void updateStatusIn( boolean success )
{ {
Context context = XWApp.getContext(); Context context = XWApp.getContext();
ConnStatusHandler ConnStatusHandler
.updateStatusIn( context, null, CommsConnType.COMMS_CONN_BT, success ); .updateStatusIn( context, CommsConnType.COMMS_CONN_BT, success );
} }
private static class KillerIn extends Thread implements AutoCloseable { private static class KillerIn extends Thread implements AutoCloseable {

View file

@ -72,12 +72,13 @@ 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;
public class BoardDelegate extends DelegateBase public class BoardDelegate extends DelegateBase
implements TransportProcs.TPMsgHandler, View.OnClickListener, implements TransportProcs.TPMsgHandler, View.OnClickListener,
DwnldDelegate.DownloadFinishedListener, DwnldDelegate.DownloadFinishedListener,
ConnStatusHandler.ConnStatusCBacks, ConnStatusHandler.ConnStatusCBacks,
NFCUtils.NFCActor { Wrapper.Procs {
private static final String TAG = BoardDelegate.class.getSimpleName(); private static final String TAG = BoardDelegate.class.getSimpleName();
private static final int SCREEN_ON_TIME = 10 * 60 * 1000; // 10 mins private static final int SCREEN_ON_TIME = 10 * 60 * 1000; // 10 mins
@ -126,6 +127,8 @@ public class BoardDelegate extends DelegateBase
private DBAlert m_inviteAlert; private DBAlert m_inviteAlert;
private boolean m_haveStartedShowing; private boolean m_haveStartedShowing;
private Wrapper mNFCWrapper;
public class TimerRunnable implements Runnable { public class TimerRunnable implements Runnable {
private int m_why; private int m_why;
private int m_when; private int m_when;
@ -170,7 +173,7 @@ public class BoardDelegate extends DelegateBase
private boolean alertOrderAt( StartAlertOrder ord ) private boolean alertOrderAt( StartAlertOrder ord )
{ {
boolean result = m_mySIS.mAlertOrder == ord; boolean result = m_mySIS.mAlertOrder == ord;
Log.d( TAG, "alertOrderAt(%s) => %b", ord, result ); // Log.d( TAG, "alertOrderAt(%s) => %b", ord, result );
return result; return result;
} }
@ -558,6 +561,9 @@ public class BoardDelegate extends DelegateBase
m_isFirstLaunch = null == savedInstanceState; m_isFirstLaunch = null == savedInstanceState;
getBundledData( savedInstanceState ); getBundledData( savedInstanceState );
int devID = DevID.getNFCDevID( m_activity );
mNFCWrapper = Wrapper.init( m_activity, this, devID );
m_utils = new BoardUtilCtxt(); m_utils = new BoardUtilCtxt();
m_timers = new TimerRunnable[4]; // needs to be in sync with m_timers = new TimerRunnable[4]; // needs to be in sync with
// XWTimerReason // XWTimerReason
@ -601,9 +607,6 @@ public class BoardDelegate extends DelegateBase
m_jniThreadRef.setDaemonOnce( true ); m_jniThreadRef.setDaemonOnce( true );
m_jniThreadRef.startOnce(); m_jniThreadRef.startOnce();
// Don't seem to need to unregister...
NFCUtils.register( m_activity, BoardDelegate.this );
setBackgroundColor(); setBackgroundColor();
setKeepScreenOn(); setKeepScreenOn();
@ -633,6 +636,7 @@ public class BoardDelegate extends DelegateBase
protected void onResume() protected void onResume()
{ {
super.onResume(); super.onResume();
Wrapper.setResumed( mNFCWrapper, true );
if ( null != m_jniThreadRef ) { if ( null != m_jniThreadRef ) {
doResume( false ); doResume( false );
} else { } else {
@ -642,6 +646,7 @@ public class BoardDelegate extends DelegateBase
protected void onPause() protected void onPause()
{ {
Wrapper.setResumed( mNFCWrapper, false );
closeIfFinishing( false ); closeIfFinishing( false );
m_handler = null; m_handler = null;
ConnStatusHandler.setHandler( null ); ConnStatusHandler.setHandler( null );
@ -1098,7 +1103,8 @@ public class BoardDelegate extends DelegateBase
launchLookup( m_mySIS.words, m_gi.dictLang ); launchLookup( m_mySIS.words, m_gi.dictLang );
break; break;
case NFC_TO_SELF: case NFC_TO_SELF:
GamesListDelegate.sendNFCToSelf( m_activity, makeNFCMessage() ); Assert.assertFalse( BuildConfig.DEBUG );
// GamesListDelegate.sendNFCToSelf( m_activity, makeNFCMessage() );
break; break;
case DROP_RELAY_ACTION: case DROP_RELAY_ACTION:
dropConViaAndRestart(CommsConnType.COMMS_CONN_RELAY); dropConViaAndRestart(CommsConnType.COMMS_CONN_RELAY);
@ -1515,22 +1521,49 @@ public class BoardDelegate extends DelegateBase
} }
////////////////////////////////////////////////// //////////////////////////////////////////////////
// NFCUtils.NFCActor // ConnStatusHandler.ConnStatusCBacks
////////////////////////////////////////////////// //////////////////////////////////////////////////
@Override @Override
public String makeNFCMessage() public void invalidateParent()
{ {
Log.d( TAG, "makeNFCMessage(): m_mySIS.nMissing: %d", m_mySIS.nMissing ); runOnUiThread(new Runnable() {
String data = null; @Override
public void run() {
m_view.invalidate();
}
});
}
@Override
public void onStatusClicked()
{
onStatusClicked( m_jniGamePtr );
}
@Override
public Handler getHandler()
{
return m_handler;
}
////////////////////////////////////////////////////////////
// NFCCardService.Wrapper.Procs
////////////////////////////////////////////////////////////
@Override
public void onReadingChange( boolean nowReading )
{
// Do we need this?
}
private byte[] getInvite()
{
byte[] result = null;
if ( 0 < m_mySIS.nMissing // Isn't there a better test?? if ( 0 < m_mySIS.nMissing // Isn't there a better test??
&& DeviceRole.SERVER_ISSERVER == m_gi.serverRole ) { && DeviceRole.SERVER_ISSERVER == m_gi.serverRole ) {
Log.d( TAG, "makeNFCMessage(): invite case" );
NetLaunchInfo nli = new NetLaunchInfo( m_gi ); NetLaunchInfo nli = new NetLaunchInfo( m_gi );
Assert.assertTrue( 0 <= m_nGuestDevs ); Assert.assertTrue( 0 <= m_nGuestDevs );
nli.forceChannel = 1 + m_nGuestDevs; nli.forceChannel = 1 + m_nGuestDevs;
Assert.assertTrue( m_connTypes.contains( CommsConnType.COMMS_CONN_NFC ) );
for ( Iterator<CommsConnType> iter = m_connTypes.iterator(); for ( Iterator<CommsConnType> iter = m_connTypes.iterator();
iter.hasNext(); ) { iter.hasNext(); ) {
CommsConnType typ = iter.next(); CommsConnType typ = iter.next();
@ -1558,46 +1591,9 @@ public class BoardDelegate extends DelegateBase
typ.toString() ); typ.toString() );
} }
} }
data = nli.makeLaunchJSON(); result = nli.asByteArray();
if ( null != data ) {
recordInviteSent( InviteMeans.NFC, null );
}
} else if ( BuildConfig.MOVE_VIA_NFC ) {
Log.d( TAG, "makeNFCMessage(): move case" );
byte[][] msgs = XwJNI.comms_getPending( m_jniGamePtr );
data = NFCUtils.makeMsgsJSON( m_gi.gameID, msgs );
} else {
Log.d( TAG, "makeNFCMessage(): other (bad!!) case" );
Assert.assertFalse( BuildConfig.DEBUG );
} }
Log.d( TAG, "makeNFCMessage() => %s", data ); return result;
return data;
}
//////////////////////////////////////////////////
// ConnStatusHandler.ConnStatusCBacks
//////////////////////////////////////////////////
@Override
public void invalidateParent()
{
runOnUiThread(new Runnable() {
@Override
public void run() {
m_view.invalidate();
}
});
}
@Override
public void onStatusClicked()
{
onStatusClicked( m_jniGamePtr );
}
@Override
public Handler getHandler()
{
return m_handler;
} }
private void launchPhoneNumberInvite( int nMissing, SentInvitesInfo info, private void launchPhoneNumberInvite( int nMissing, SentInvitesInfo info,
@ -2269,8 +2265,15 @@ public class BoardDelegate extends DelegateBase
if ( null == m_jniThread ) { if ( null == m_jniThread ) {
m_jniThread = m_jniThreadRef.retain(); m_jniThread = m_jniThreadRef.retain();
m_gi = m_jniThread.getGI(); m_gi = m_jniThread.getGI();
m_summary = m_jniThread.getSummary(); m_summary = m_jniThread.getSummary();
Wrapper.setGameID( mNFCWrapper, m_gi.gameID );
byte[] invite = getInvite();
if ( null != invite ) {
NFCUtils.addInvitationFor( invite, m_gi.gameID );
}
m_view.startHandling( m_activity, m_jniThread, m_connTypes ); m_view.startHandling( m_activity, m_jniThread, m_connTypes );
handleViaThread( JNICmd.CMD_START ); handleViaThread( JNICmd.CMD_START );

View file

@ -181,7 +181,7 @@ public class CommsTransport implements TransportProcs,
addIncoming(); addIncoming();
} }
ConnStatusHandler. ConnStatusHandler.
updateStatusIn( m_context, null, updateStatusIn( m_context,
CommsConnType.COMMS_CONN_RELAY, CommsConnType.COMMS_CONN_RELAY,
0 <= nRead ); 0 <= nRead );
} }
@ -190,7 +190,7 @@ public class CommsTransport implements TransportProcs,
if ( null != m_bytesOut ) { if ( null != m_bytesOut ) {
int nWritten = channel.write( m_bytesOut ); int nWritten = channel.write( m_bytesOut );
ConnStatusHandler. ConnStatusHandler.
updateStatusOut( m_context, null, updateStatusOut( m_context,
CommsConnType.COMMS_CONN_RELAY, CommsConnType.COMMS_CONN_RELAY,
0 < nWritten ); 0 < nWritten );
} }
@ -444,6 +444,7 @@ public class CommsTransport implements TransportProcs,
.sendPacket( context, addr.p2p_addr, gameID, buf ); .sendPacket( context, addr.p2p_addr, gameID, buf );
break; break;
case COMMS_CONN_NFC: case COMMS_CONN_NFC:
nSent = NFCUtils.addMsgFor( buf, gameID );
break; break;
default: default:
Assert.fail(); Assert.fail();

View file

@ -178,10 +178,6 @@ public class ConnStatusHandler {
R.string.connstat_net_fmt, R.string.connstat_net_fmt,
connTypes.toString( context, true ))); connTypes.toString( context, true )));
for ( CommsConnType typ : connTypes.getTypes() ) { for ( CommsConnType typ : connTypes.getTypes() ) {
if ( ! typ.isSelectable() ) {
continue;
}
sb.append( String.format( "\n\n*** %s ", typ.longName( context ) ) ); sb.append( String.format( "\n\n*** %s ", typ.longName( context ) ) );
String did = addDebugInfo( context, gamePtr, addr, typ ); String did = addDebugInfo( context, gamePtr, addr, typ );
if ( null != did ) { if ( null != did ) {
@ -363,12 +359,23 @@ public class ConnStatusHandler {
updateStatusImpl( context, cbacks, connType, success, true ); updateStatusImpl( context, cbacks, connType, success, true );
} }
public static void updateStatusIn( Context context, CommsConnType connType,
boolean success )
{
updateStatusImpl( context, null, connType, success, true );
}
public static void updateStatusOut( Context context, ConnStatusCBacks cbacks, public static void updateStatusOut( Context context, ConnStatusCBacks cbacks,
CommsConnType connType, boolean success ) CommsConnType connType, boolean success )
{ {
updateStatusImpl( context, cbacks, connType, success, false ); updateStatusImpl( context, cbacks, connType, success, false );
} }
public static void updateStatusOut( Context context, CommsConnType connType, boolean success )
{
updateStatusImpl( context, null, connType, success, false );
}
private static void updateStatusImpl( Context context, ConnStatusCBacks cbacks, private static void updateStatusImpl( Context context, ConnStatusCBacks cbacks,
CommsConnType connType, boolean success, CommsConnType connType, boolean success,
boolean isIn ) boolean isIn )

View file

@ -158,14 +158,18 @@ public class DbgUtils {
// return TextUtils.join( ", ", asStrs ); // return TextUtils.join( ", ", asStrs );
// } // }
// public static String hexDump( byte[] bytes ) public static String hexDump( byte[] bytes )
// { {
// StringBuilder dump = new StringBuilder(); String result = "<null>";
// for ( byte byt : bytes ) { if ( null != bytes ) {
// dump.append( String.format( "%02x ", byt ) ); StringBuilder dump = new StringBuilder();
// } for ( byte byt : bytes ) {
// return dump.toString(); dump.append( String.format( "%02x ", byt ) );
// } }
result = dump.toString();
}
return result;
}
private static List<DeadlockWatch> sLockHolders = new ArrayList<>(); private static List<DeadlockWatch> sLockHolders = new ArrayList<>();

View file

@ -37,6 +37,7 @@ public class DevID {
private static final String DEVID_KEY = "DevID.devid_key"; private static final String DEVID_KEY = "DevID.devid_key";
private static final String DEVID_ACK_KEY = "key_relay_regid_ackd2"; private static final String DEVID_ACK_KEY = "key_relay_regid_ackd2";
private static final String FCM_REGVERS_KEY = "key_fcmvers_regid"; private static final String FCM_REGVERS_KEY = "key_fcmvers_regid";
private static final String NFC_DEVID_KEY = "key_nfc_devid";
private static String s_relayDevID; private static String s_relayDevID;
private static int s_asInt; private static int s_asInt;
@ -145,4 +146,22 @@ public class DevID {
{ {
DBUtils.setBoolFor( context, DEVID_ACK_KEY, false ); DBUtils.setBoolFor( context, DEVID_ACK_KEY, false );
} }
// Just a random number I hang onto as long as possible
private static int[] sNFCDevID = {0};
public static int getNFCDevID( Context context )
{
synchronized ( sNFCDevID ) {
if ( 0 == sNFCDevID[0] ) {
int devid = DBUtils.getIntFor( context, NFC_DEVID_KEY, 0 );
if ( 0 == devid ) {
devid = Utils.nextRandomInt();
DBUtils.setIntFor( context, NFC_DEVID_KEY, devid );
}
sNFCDevID[0] = devid;
}
Log.d( TAG, "getNFCDevID() => %d", sNFCDevID[0] );
return sNFCDevID[0];
}
}
} }

View file

@ -249,7 +249,7 @@ public class GameListItem extends LinearLayout
case R.string.game_summary_field_empty: case R.string.game_summary_field_empty:
break; break;
case R.string.game_summary_field_gameid: case R.string.game_summary_field_gameid:
value = String.format( "%X", m_summary.gameID ); value = String.format( "%d", m_summary.gameID );
break; break;
case R.string.game_summary_field_rowid: case R.string.game_summary_field_rowid:
value = String.format( "%d", m_rowid ); value = String.format( "%d", m_rowid );

View file

@ -64,6 +64,7 @@ import org.eehouse.android.xw4.loc.LocUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
@ -2353,14 +2354,14 @@ public class GamesListDelegate extends ListDelegateBase
private boolean tryNFCIntent( Intent intent ) private boolean tryNFCIntent( Intent intent )
{ {
boolean result = false; boolean result = false;
String data = NFCUtils.getFromIntent( intent ); byte[] data = NFCUtils.getFromIntent( intent );
if ( null != data ) { if ( null != data ) {
NetLaunchInfo nli = NetLaunchInfo.makeFrom( m_activity, data ); NetLaunchInfo nli = NetLaunchInfo.makeFrom( m_activity, data );
if ( null != nli && nli.isValid() ) { if ( null != nli && nli.isValid() ) {
startNewNetGame( nli ); startNewNetGame( nli );
result = true; result = true;
} else { } else {
NFCUtils.receiveMsgs( m_activity, data ); Assert.assertFalse( BuildConfig.DEBUG );
} }
} }
return result; return result;
@ -2843,10 +2844,12 @@ public class GamesListDelegate extends ListDelegateBase
; ;
} }
public static void sendNFCToSelf( Context context, String data ) public static void postNFCInvite( Context context, byte[] data )
{ {
Intent intent = makeSelfIntent( context ); Intent intent = makeSelfIntent( context )
NFCUtils.populateIntent( intent, data ); .addFlags( Intent.FLAG_ACTIVITY_NEW_TASK )
;
NFCUtils.populateIntent( context, intent, data );
context.startActivity( intent ); context.startActivity( intent );
} }

View file

@ -79,6 +79,12 @@ public class MultiMsgSink implements TransportProcs {
.sendPacket( m_context, addr.p2p_addr, gameID, buf ); .sendPacket( m_context, addr.p2p_addr, gameID, buf );
} }
int sendViaNFC( byte[] buf, int gameID )
{
Log.d( TAG, "sendViaNFC(gameID=%d, len=%d)", gameID, buf.length );
return NFCUtils.addMsgFor( buf, gameID );
}
public int numSent() public int numSent()
{ {
return m_sentSet.size(); return m_sentSet.size();
@ -108,12 +114,13 @@ public class MultiMsgSink implements TransportProcs {
break; break;
case COMMS_CONN_NFC: case COMMS_CONN_NFC:
Log.d( TAG, "transportSend(): got for NFC" ); Log.d( TAG, "transportSend(): got for NFC" );
nSent = sendViaNFC( buf, gameID );
break; break;
default: default:
Assert.fail(); Assert.fail();
break; break;
} }
Log.i( TAG, "transportSend(): sent %d msgs for game %d/%x via %s", Log.i( TAG, "transportSend(): sent %d bytes for game %d/%x via %s",
nSent, gameID, gameID, typ.toString() ); nSent, gameID, gameID, typ.toString() );
if ( 0 < nSent ) { if ( 0 < nSent ) {
Log.d( TAG, "transportSend: adding %s", msgID ); Log.d( TAG, "transportSend: adding %s", msgID );

View file

@ -66,8 +66,7 @@ public class NBSProto {
DbgUtils.showf( context, "Got msg %d", s_nReceived ); DbgUtils.showf( context, "Got msg %d", s_nReceived );
} }
ConnStatusHandler.updateStatusIn( context, null, ConnStatusHandler.updateStatusIn( context, CommsConnType.COMMS_CONN_SMS,
CommsConnType.COMMS_CONN_SMS,
true ); true );
} }
@ -380,8 +379,7 @@ public class NBSProto {
DbgUtils.showf( context, "Sent msg %d", s_nSent ); DbgUtils.showf( context, "Sent msg %d", s_nSent );
} }
ConnStatusHandler.updateStatusOut( context, null, ConnStatusHandler.updateStatusOut( context, CommsConnType.COMMS_CONN_SMS,
CommsConnType.COMMS_CONN_SMS,
success ); success );
} }

View file

@ -0,0 +1,795 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2019 by Eric House (xwords@eehouse.org). All rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
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.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<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
@Override
public void onCreate()
{
super.onCreate();
mMyDevID = DevID.getNFCDevID( this );
Log.d( TAG, "onCreate() got mydevid %d", mMyDevID );
}
private int mGameID;
@Override
public byte[] processCommandApdu( byte[] apdu, Bundle extras )
{
// Log.d( TAG, "processCommandApdu(%s)", DbgUtils.hexDump(apdu ) );
HEX_STR resStr = HEX_STR.STATUS_FAILED;
boolean isAidCase = false;
if ( null != apdu ) {
if ( HEX_STR.CMD_MSG_PART.matchesFrom( apdu ) ) {
resStr = HEX_STR.STATUS_SUCCESS;
int[] msgID = {0};
byte[] all = reassemble( this, apdu, msgID, HEX_STR.CMD_MSG_PART );
if ( null != all ) {
addToMsgThread( this, all );
setLatestAck( msgID[0] );
}
} else {
Log.d( TAG, "processCommandApdu(): aid case?" );
if ( ! HEX_STR.DEFAULT_CLA.matchesFrom( apdu ) ) {
resStr = HEX_STR.CLA_NOT_SUPPORTED;
} else if ( ! HEX_STR.SELECT_INS.matchesFrom( apdu, 1 ) ) {
resStr = HEX_STR.INS_NOT_SUPPORTED;
} else if ( LEN_OFFSET >= apdu.length ) {
Log.d( TAG, "processCommandApdu(): apdu too short" );
// Not long enough for length byte
} else {
try {
ByteArrayInputStream bais
= new ByteArrayInputStream( apdu, LEN_OFFSET,
apdu.length - LEN_OFFSET );
byte aidLen = (byte)bais.read();
Log.d( TAG, "aidLen=%d", aidLen );
if ( bais.available() >= aidLen + 1 ) {
byte[] aidPart = new byte[aidLen];
bais.read( aidPart );
String aidStr = Utils.ba2HexStr( aidPart );
if ( BuildConfig.NFC_AID.equals( aidStr ) ) {
byte minVersion = (byte)bais.read();
byte maxVersion = (byte)bais.read();
if ( minVersion == VERSION_1 ) {
int devID = numFrom( bais );
Log.d( TAG, "processCommandApdu(): read "
+ "remote devID: %d", devID );
mGameID = numFrom( bais );
Log.d( TAG, "read gameID: %d", mGameID );
if ( 0 < bais.available() ) {
Log.d( TAG, "processCommandApdu(): "
+ "leaving anything behind?" );
}
resStr = HEX_STR.STATUS_SUCCESS;
isAidCase = true;
} else {
Log.e( TAG, "unexpected version %d; I'm too old?",
minVersion );
}
} else {
Log.e( TAG, "aid mismatch: got %s but wanted %s",
aidStr, BuildConfig.NFC_AID );
}
}
} catch ( IOException ioe ) {
Assert.assertFalse( BuildConfig.DEBUG );
}
}
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
baos.write( resStr.asBA() );
if ( HEX_STR.STATUS_SUCCESS == resStr ) {
if ( isAidCase ) {
baos.write( VERSION_1 ); // min
baos.write( numTo( mMyDevID ) );
} else {
MsgToken token = NFCUtils.getMsgsFor( mGameID );
byte[][] tmp = wrapMsg( token, Short.MAX_VALUE );
Assert.assertTrue( 1 == tmp.length || !BuildConfig.DEBUG );
baos.write( tmp[0] );
}
}
} catch ( IOException ioe ) {
Assert.assertFalse( BuildConfig.DEBUG );
}
byte[] result = baos.toByteArray();
Log.d( TAG, "processCommandApdu(%s) => %s", DbgUtils.hexDump( apdu ),
DbgUtils.hexDump( result ) );
// this comes out of transceive() below!!!
return result;
} // processCommandApdu
@Override
public void onDeactivated( int reason )
{
String str = "<other>";
switch ( reason ) {
case HostApduService.DEACTIVATION_LINK_LOSS:
str = "DEACTIVATION_LINK_LOSS";
break;
case HostApduService.DEACTIVATION_DESELECTED:
str = "DEACTIVATION_DESELECTED";
break;
}
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,
int[] msgIDOut, HEX_STR cmd )
{
return reassemble( context, part, msgIDOut, cmd.length() );
}
private synchronized static byte[] reassemble( Context context, byte[] part,
int[] msgIDOut, int offset )
{
part = Arrays.copyOfRange( part, offset, part.length );
return reassemble( context, part, msgIDOut );
}
private synchronized static byte[] reassemble( Context context,
byte[] part, int[] msgIDOut )
{
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();
msgIDOut[0] = sMsgID;
if ( 0 != sMsgID ) {
Log.d( TAG, "reassemble(): done reassembling msgID=%d: %s",
msgIDOut[0], 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 );
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;
if ( null != NfcAdapter.getDefaultAdapter( activity ) ) {
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.setResumed( resumed );
}
}
static void setGameID( Wrapper instance, int gameID )
{
if ( null != instance ) {
instance.setGameID( gameID );
}
}
private Wrapper( Activity activity, 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 ) ) {
int[] msgID = {0};
byte[] all = reassemble( mActivity, response, msgID,
offset + HEX_STR.CMD_MSG_PART.length() );
if ( null != all ) {
addToMsgThread( mActivity, all );
setLatestAck( msgID[0] );
}
}
}
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" );
}
// toggle();
// try {
// // How long to sleep.
// int intervalMS = mMinMS + (Math.abs(mRandom.nextInt())
// % (mMaxMS - mMinMS));
// // Log.d( TAG, "sleeping for %d ms", intervalMS );
// 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 );
}
}
}
}
}

View file

@ -25,20 +25,27 @@ import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
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.os.Build; import android.os.Build;
import android.os.Parcelable; import android.os.Parcelable;
import org.json.JSONArray; import java.io.ByteArrayInputStream;
import org.json.JSONObject; import java.io.ByteArrayOutputStream;
import org.json.JSONException; import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.MultiService.MultiEvent;
import org.eehouse.android.xw4.jni.CommsAddrRec; import org.eehouse.android.xw4.jni.CommsAddrRec;
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();
@ -46,53 +53,14 @@ public class NFCUtils {
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";
private static final String MSGS = "MSGS"; private static final byte MESSAGE = 0x01;
private static final String GAMEID = "GAMEID"; private static final byte INVITE = 0x02;
private static final byte REPLY = 0x03;
public interface NFCActor { private static final byte REPLY_NOGAME = 0x00;
String makeNFCMessage();
}
private static boolean s_inSDK; private static boolean s_inSDK = 14 <= Build.VERSION.SDK_INT;
private static boolean[] s_nfcAvail; private static boolean[] s_nfcAvail;
private static SafeNFC s_safeNFC;
static {
s_inSDK = 14 <= Build.VERSION.SDK_INT
&& Build.VERSION.SDK_INT <= Build.VERSION_CODES.P;
if ( s_inSDK ) {
s_safeNFC = new SafeNFCImpl();
}
}
private static interface SafeNFC {
public void register( Activity activity, NFCActor actor );
}
private static class SafeNFCImpl implements SafeNFC {
public void register( final Activity activity, final NFCActor actor )
{
NfcManager manager =
(NfcManager)activity.getSystemService( Context.NFC_SERVICE );
if ( null != manager ) {
NfcAdapter adapter = manager.getDefaultAdapter();
if ( null != adapter ) {
NfcAdapter.CreateNdefMessageCallback cb =
new NfcAdapter.CreateNdefMessageCallback() {
public NdefMessage createNdefMessage( NfcEvent evt )
{
NdefMessage msg = null;
String data = actor.makeNFCMessage();
if ( null != data ) {
msg = makeMessage( activity, data );
}
return msg;
}
};
adapter.setNdefPushMessageCallback( cb, activity );
}
}
}
}
// Return array of two booleans, the first indicating whether the // Return array of two booleans, the first indicating whether the
// device supports NFC and the second whether it's on. Only the // device supports NFC and the second whether it's on. Only the
@ -108,38 +76,32 @@ public class NFCUtils {
if ( s_nfcAvail[0] ) { if ( s_nfcAvail[0] ) {
s_nfcAvail[1] = getNFCAdapter( context ).isEnabled(); s_nfcAvail[1] = getNFCAdapter( context ).isEnabled();
} }
// Log.d( TAG, "nfcAvail() => {%b,%b}", s_nfcAvail[0], s_nfcAvail[1] );
return s_nfcAvail; return s_nfcAvail;
} }
public static String getFromIntent( Intent intent ) public static byte[] getFromIntent( Intent intent )
{ {
String result = null; byte[] result = null;
String action = intent.getAction(); String action = intent.getAction();
if ( NfcAdapter.ACTION_NDEF_DISCOVERED.equals( action ) ) { if ( NFC_TO_SELF_ACTION.equals( action ) ) {
Parcelable[] rawMsgs = result = intent.getByteArrayExtra( NFC_TO_SELF_DATA );
intent.getParcelableArrayExtra( NfcAdapter.EXTRA_NDEF_MESSAGES );
// only one message sent during the beam
NdefMessage msg = (NdefMessage)rawMsgs[0];
// record 0 contains the MIME type, record 1 is the AAR, if present
result = new String( msg.getRecords()[0].getPayload() );
} else if ( NFC_TO_SELF_ACTION.equals( action ) ) {
result = intent.getStringExtra( NFC_TO_SELF_DATA );
} }
Log.d( TAG, "getFromIntent() => %s", result );
return result; return result;
} }
public static void populateIntent( Intent intent, String data ) public static void populateIntent( Context context, Intent intent,
byte[] data )
{ {
intent.setAction( NFC_TO_SELF_ACTION ) NetLaunchInfo nli = NetLaunchInfo.makeFrom( context, data );
.putExtra( NFC_TO_SELF_DATA, data ); if ( null != nli ) {
} intent.setAction( NFC_TO_SELF_ACTION )
.putExtra( NFC_TO_SELF_DATA, data );
public static void register( Activity activity, NFCActor actor ) } else {
{ Assert.assertFalse( BuildConfig.DEBUG );
if ( null != s_safeNFC ) {
s_safeNFC.register( activity, actor );
} }
} }
@ -162,19 +124,6 @@ public class NFCUtils {
.create(); .create();
} }
private static NdefMessage makeMessage( Activity activity, String data )
{
String mimeType = LocUtils.getString( activity, R.string.xwords_nfc_mime );
NdefMessage msg = new NdefMessage( new NdefRecord[] {
new NdefRecord(NdefRecord.TNF_MIME_MEDIA,
mimeType.getBytes(), new byte[0],
data.getBytes())
,NdefRecord.
createApplicationRecord( activity.getPackageName() )
});
return msg;
}
private static NfcAdapter getNFCAdapter( Context context ) private static NfcAdapter getNFCAdapter( Context context )
{ {
NfcManager manager = NfcManager manager =
@ -182,44 +131,303 @@ public class NFCUtils {
return manager.getDefaultAdapter(); return manager.getDefaultAdapter();
} }
static String makeMsgsJSON( int gameID, byte[][] msgs ) private static byte[] formatMsgs( int gameID, List<byte[]> msgs )
{ {
String result = null; return formatMsgs( gameID, msgs.toArray( new byte[msgs.size()][] ) );
}
JSONArray arr = new JSONArray(); private static byte[] formatMsgs( int gameID, byte[][] msgs )
for ( byte[] msg : msgs ) { {
arr.put( Utils.base64Encode( msg ) ); byte[] result = null;
}
if ( null != msgs && 0 < msgs.length ) {
try { try {
JSONObject obj = new JSONObject(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
obj.put( MSGS, arr ); DataOutputStream dos = new DataOutputStream( baos );
obj.put( GAMEID, gameID ); dos.writeInt( gameID );
Log.d( TAG, "formatMsgs(): wrote gameID: %d", gameID );
result = obj.toString(); dos.flush();
} catch ( JSONException ex ) { baos.write( msgs.length );
Assert.assertFalse( BuildConfig.DEBUG ); for ( int ii = 0; ii < msgs.length; ++ii ) {
byte[] msg = msgs[ii];
short len = (short)msg.length;
baos.write( len & 0xFF );
baos.write( (len >> 8) & 0xFF );
baos.write( msg );
}
result = baos.toByteArray();
} catch ( IOException ioe ) {
Assert.assertFalse( BuildConfig.DEBUG );
}
} }
Log.d( TAG, "formatMsgs(gameID=%d) => %s", gameID, DbgUtils.hexDump( result ) );
return result; return result;
} }
static boolean receiveMsgs( Context context, String data ) private static byte[][] unformatMsgs( byte[] data, int start, int[] gameID )
{ {
Log.d( TAG, "receiveMsgs()" ); byte[][] result = null;
int gameID[] = {0}; try {
byte[][] msgs = msgsFrom( data, gameID ); ByteArrayInputStream bais
boolean success = null != msgs && 0 < msgs.length; = new ByteArrayInputStream( data, start, data.length );
if ( success ) { DataInputStream dis = new DataInputStream( bais );
NFCServiceHelper helper = new NFCServiceHelper( context ); gameID[0] = dis.readInt();
long[] rowids = DBUtils.getRowIDsFor( context, gameID[0] ); Log.d( TAG, "unformatMsgs(): read gameID: %d", gameID[0] );
for ( long rowid : rowids ) { int count = bais.read();
NFCMsgSink sink = new NFCMsgSink( context, rowid ); Log.d( TAG, "unformatMsgs(): read count: %d", count );
for ( byte[] msg : msgs ) { result = new byte[count][];
helper.receiveMessage( rowid, sink, msg );
for ( int ii = 0; ii < count; ++ii ) {
short len = (short)bais.read();
len |= (int)(bais.read() << 8);
Log.d( TAG, "unformatMsgs(): read len %d for msg %d", len, ii );
byte[] msg = new byte[len];
int nRead = bais.read( msg );
Assert.assertTrue( nRead == msg.length );
result[ii] = msg;
}
} catch ( IOException ex ) {
Log.d( TAG, "ex: %s: %s", ex, ex.getMessage() );
result = null;
gameID[0] = 0;
}
Log.d( TAG, "unformatMsgs() => %s (len=%d)", result,
null == result ? 0 : result.length );
return result;
}
interface HaveDataListener {
void onHaveDataChanged( boolean nowHaveData );
}
public static class MsgToken {
private MsgsStore mStore;
private byte[][] mMsgs;
private int mGameID;
private MsgToken( MsgsStore store, int gameID )
{
mStore = store;
mGameID = gameID;
mMsgs = mStore.getMsgsFor( gameID );
}
byte[] getMsgs()
{
return formatMsgs( mGameID, mMsgs );
}
void removeSentMsgs()
{
mStore.removeSentMsgs( mGameID, mMsgs );
}
}
private static class MsgsStore {
private Map<Integer, WeakReference<HaveDataListener>> mListeners
= new HashMap<>();
private static Map<Integer, List<byte[]>> mMsgMap = new HashMap<>();
void setHaveDataListener( int gameID, HaveDataListener listener )
{
Assert.assertFalse( gameID == 0 );
WeakReference<HaveDataListener> ref = new WeakReference<>(listener);
synchronized ( mListeners ) {
mListeners.put( gameID, ref );
}
byte[][] msgs = getMsgsFor( gameID );
listener.onHaveDataChanged( null != msgs && 0 < msgs.length );
}
private int addMsgFor( int gameID, byte typ, byte[] msg )
{
Boolean nowHaveData = null;
synchronized ( mMsgMap ) {
if ( !mMsgMap.containsKey( gameID ) ) {
mMsgMap.put( gameID, new ArrayList<byte[]>() );
}
List<byte[]> msgs = mMsgMap.get( gameID );
byte[] full = new byte[msg.length + 1];
full[0] = typ;
System.arraycopy( msg, 0, full, 1, msg.length );
// Can't use msgs.contains() because it uses equals()
boolean isDuplicate = false;
for ( byte[] curMsg : msgs ) {
if ( Arrays.equals( curMsg, full ) ) {
isDuplicate = true;
break;
}
}
if ( !isDuplicate ) {
msgs.add( full );
nowHaveData = 0 < msgs.size();
Log.d( TAG, "addMsgFor(gameID=%d): added %s; now have %d msgs",
gameID, DbgUtils.hexDump(msg), msgs.size() );
}
}
reportHaveData( gameID, nowHaveData );
return msg.length;
}
private byte[][] getMsgsFor( int gameID )
{
Assert.assertFalse( gameID == 0 );
byte[][] result = null;
synchronized ( mMsgMap ) {
if ( mMsgMap.containsKey( gameID ) ) {
List<byte[]> msgs = mMsgMap.get( gameID );
result = msgs.toArray( new byte[msgs.size()][] );
}
}
Log.d( TAG, "getMsgsFor() => %d msgs", result == null ? 0 : result.length );
return result;
}
private void removeSentMsgs( int gameID, byte[][] msgs )
{
Boolean nowHaveData = null;
if ( null != msgs ) {
synchronized ( mMsgMap ) {
if ( mMsgMap.containsKey( gameID ) ) {
List<byte[]> list = mMsgMap.get( gameID );
// Log.d( TAG, "removeSentMsgs(%d): size before: %d", gameID,
// list.size() );
int origSize = list.size();
for ( byte[] msg : msgs ) {
list.remove( msg );
}
if ( 0 < origSize ) {
Log.d( TAG, "removeSentMsgs(%d): size was %d, now %d", gameID,
origSize, list.size() );
}
nowHaveData = 0 < list.size();
}
}
}
reportHaveData( gameID, nowHaveData );
}
private void reportHaveData( int gameID, Boolean nowHaveData )
{
Log.d( TAG, "reportHaveData(" + nowHaveData + ")" );
if ( null != nowHaveData ) {
HaveDataListener proc = null;
synchronized ( mListeners ) {
WeakReference<HaveDataListener> ref = mListeners.get( gameID );
if ( null != ref ) {
proc = ref.get();
if ( null == proc ) {
mListeners.remove( gameID );
}
} else {
Log.d( TAG, "reportHaveData(): no listener for %d", gameID );
}
}
if ( null != proc ) {
proc.onHaveDataChanged( nowHaveData );
}
}
}
static byte[] split( byte[] msg, byte[] headerOut )
{
headerOut[0] = msg[0];
byte[] result = Arrays.copyOfRange( msg, 1, msg.length );
Log.d( TAG, "split(%s) => %d/%s", DbgUtils.hexDump( msg ),
headerOut[0], DbgUtils.hexDump( result ) );
return result;
}
}
private static MsgsStore sMsgsStore = new MsgsStore();
static void setHaveDataListener( int gameID, HaveDataListener listener )
{
sMsgsStore.setHaveDataListener( gameID, listener );
}
static int addMsgFor( byte[] msg, int gameID )
{
return sMsgsStore.addMsgFor( gameID, MESSAGE, msg );
}
static int addInvitationFor( byte[] msg, int gameID )
{
return sMsgsStore.addMsgFor( gameID, INVITE, msg );
}
static int addReplyFor( byte[] msg, int gameID )
{
return sMsgsStore.addMsgFor( gameID, REPLY, msg );
}
static MsgToken getMsgsFor( int gameID )
{
MsgToken token = new MsgToken( sMsgsStore, gameID );
return token;
}
static void receiveMsgs( Context context, byte[] data )
{
receiveMsgs( context, data, 0 );
}
static void receiveMsgs( Context context, byte[] data, int offset )
{
// Log.d( TAG, "receiveMsgs(gameID=%d, %s, offset=%d)", gameID,
// DbgUtils.hexDump(data), offset );
DbgUtils.assertOnUIThread( false );
int[] gameID = {0};
byte[][] msgs = unformatMsgs( data, offset, gameID );
if ( null != msgs ) {
NFCServiceHelper helper = new NFCServiceHelper( context );
for ( byte[] msg : msgs ) {
byte[] typ = {0};
byte[] body = MsgsStore.split( msg, typ );
switch ( typ[0] ) {
case MESSAGE:
long[] rowids = DBUtils.getRowIDsFor( context, gameID[0] );
if ( null == rowids || 0 == rowids.length ) {
addReplyFor( new byte[]{REPLY_NOGAME}, gameID[0] );
} else {
for ( long rowid : rowids ) {
NFCMsgSink sink = new NFCMsgSink( context, rowid );
helper.receiveMessage( rowid, sink, body );
}
}
break;
case INVITE:
GamesListDelegate.postNFCInvite( context, body );
break;
case REPLY:
switch( body[0] ) {
case REPLY_NOGAME:
// PENDING Don't enable this until deviceID is being
// checked. Otherwise it'll happen every time I tap my
// device against another that doesn't have my game,
// which could be common.
// helper.postEvent( MultiEvent.MESSAGE_NOGAME, gameID );
Log.e( TAG, "receiveMsgs(): not calling helper.postEvent( "
+ "MultiEvent.MESSAGE_NOGAME, gameID );" );
break;
default:
Log.e( TAG, "unexpected reply %d", body[0] );
Assert.assertFalse( BuildConfig.DEBUG );
break;
}
break;
default:
Assert.assertFalse( BuildConfig.DEBUG );
break;
} }
} }
} }
return success;
} }
private static class NFCServiceHelper extends XWServiceHelper { private static class NFCServiceHelper extends XWServiceHelper {
@ -248,7 +456,7 @@ public class NFCUtils {
private void receiveMessage( long rowid, NFCMsgSink sink, byte[] msg ) private void receiveMessage( long rowid, NFCMsgSink sink, byte[] msg )
{ {
Log.d( TAG, "receiveMessage()" ); Log.d( TAG, "receiveMessage(rowid=%d, len=%d)", rowid, msg.length );
receiveMessage( rowid, sink, msg, mAddr ); receiveMessage( rowid, sink, msg, mAddr );
} }
} }
@ -259,27 +467,4 @@ public class NFCUtils {
super( context, rowid ); super( context, rowid );
} }
} }
private static byte[][] msgsFrom( String json, /*out*/ int[] gameID )
{
byte[][] result = null;
try {
JSONObject obj = new JSONObject( json );
gameID[0] = obj.getInt( GAMEID );
JSONArray arr = obj.getJSONArray( MSGS );
if ( null != arr ) {
result = new byte[arr.length()][];
for ( int ii = 0; ii < arr.length(); ++ii ) {
String str = arr.getString( ii );
result[ii] = Utils.base64Decode( str );
}
}
} catch ( JSONException ex ) {
Assert.assertFalse( BuildConfig.DEBUG );
result = null;
}
Log.d( TAG, "msgsFrom() => %s", (Object)result );
return result;
}
} }

View file

@ -294,6 +294,7 @@ public class NetLaunchInfo implements Serializable {
addP2PInfo( context ); addP2PInfo( context );
break; break;
case COMMS_CONN_NFC: case COMMS_CONN_NFC:
addNFCInfo();
break; break;
default: default:
Assert.fail(); Assert.fail();

View file

@ -735,9 +735,9 @@ public class RelayService extends XWJIService
Log.e( TAG, "fail sending to %s", udpSocket ); Log.e( TAG, "fail sending to %s", udpSocket );
Log.ex( TAG, ex ); Log.ex( TAG, ex );
Log.i( TAG, "Restarting threads to force new socket" ); Log.i( TAG, "Restarting threads to force new socket" );
ConnStatusHandler.updateStatusOut( service, null, ConnStatusHandler
CommsConnType.COMMS_CONN_RELAY, .updateStatusOut( service, CommsConnType.COMMS_CONN_RELAY,
true ); true );
closeUDPSocket( udpSocket ); closeUDPSocket( udpSocket );
service.m_handler.post( new Runnable() { service.m_handler.post( new Runnable() {

View file

@ -615,6 +615,40 @@ public class Utils {
return Looper.getMainLooper().equals(Looper.myLooper()); return Looper.getMainLooper().equals(Looper.myLooper());
} }
// But see hexArray above
private static final String HEX_CHARS = "0123456789ABCDEF";
private static char[] HEX_CHARS_ARRAY = HEX_CHARS.toCharArray();
public static String ba2HexStr( byte[] input )
{
StringBuffer sb = new StringBuffer();
for ( byte byt : input ) {
sb.append(HEX_CHARS_ARRAY[(byt >> 4) & 0x0F]);
sb.append(HEX_CHARS_ARRAY[byt & 0x0F]);
}
String result = sb.toString();
return result;
}
public static byte[] hexStr2ba( String data )
{
data = data.toUpperCase();
Assert.assertTrue( 0 == data.length() % 2 );
byte[] result = new byte[data.length() / 2];
for (int ii = 0; ii < data.length(); ii += 2 ) {
int one = HEX_CHARS.indexOf(data.charAt(ii));
Assert.assertTrue( one >= 0 );
int two = HEX_CHARS.indexOf(data.charAt(ii + 1));
Assert.assertTrue( two >= 0 );
result[ii/2] = (byte)((one << 4) | two);
}
return result;
}
public static String base64Encode( byte[] in ) public static String base64Encode( byte[] in )
{ {
return Base64.encodeToString( in, Base64.NO_WRAP ); return Base64.encodeToString( in, Base64.NO_WRAP );

View file

@ -172,15 +172,15 @@ public class WiDirService extends XWService {
private static void updateStatusOut( boolean success ) private static void updateStatusOut( boolean success )
{ {
ConnStatusHandler ConnStatusHandler
.updateStatusOut( XWApp.getContext(), null, .updateStatusOut( XWApp.getContext(),
CommsConnType.COMMS_CONN_P2P, success ); CommsConnType.COMMS_CONN_P2P, success );
} }
private static void updateStatusIn( boolean success ) private static void updateStatusIn( boolean success )
{ {
ConnStatusHandler ConnStatusHandler
.updateStatusIn( XWApp.getContext(), null, .updateStatusIn( XWApp.getContext(), CommsConnType.COMMS_CONN_P2P,
CommsConnType.COMMS_CONN_P2P, success ); success );
} }
public static void init( Context context ) public static void init( Context context )

View file

@ -80,6 +80,8 @@ public class CommsAddrRec {
id = R.string.invite_choice_data_sms; break; id = R.string.invite_choice_data_sms; break;
case COMMS_CONN_P2P: case COMMS_CONN_P2P:
id = R.string.invite_choice_p2p; break; id = R.string.invite_choice_p2p; break;
case COMMS_CONN_NFC:
id = R.string.invite_choice_nfc; break;
default: default:
Assert.assertFalse( BuildConfig.DEBUG ); Assert.assertFalse( BuildConfig.DEBUG );
} }

View file

@ -152,7 +152,6 @@
<!-- other --> <!-- other -->
<string name="default_host">eehouse.org</string> <string name="default_host">eehouse.org</string>
<!-- <string name="default_host">10.0.3.2</string> --> <!-- <string name="default_host">10.0.3.2</string> -->
<string name="xwords_nfc_mime">application/org.eehouse.android.xw4</string>
<string name="invite_host">eehouse.org</string> <string name="invite_host">eehouse.org</string>
<string name="invite_mime">application/x-xwordsinvite</string> <string name="invite_mime">application/x-xwordsinvite</string>
<!--string name="invite_mime">text/plain</string--> <!--string name="invite_mime">text/plain</string-->

View file

@ -2397,5 +2397,7 @@
they\'re committed as moves -- by long-tapping, same as committed they\'re committed as moves -- by long-tapping, same as committed
words.\n\nUse this feature to check the validity of words you\'re words.\n\nUse this feature to check the validity of words you\'re
thinking of playing, or to look up an unfamiliar word provided as a thinking of playing, or to look up an unfamiliar word provided as a
hint.</string> hint.</string>
<string name="servicedesc">For transmitting CrossWords moves</string>
</resources> </resources>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/servicedesc"
android:requireDeviceUnlock="false">
<aid-group android:description="@string/servicedesc"
android:category="other">
<aid-filter android:name="@string/nfc_aid"/>
</aid-group>
</host-apdu-service>

View file

@ -2647,26 +2647,27 @@ comms_getStats( CommsCtxt* comms, XWStreamCtxt* stream )
(XP_UCHAR*)"msg queue len: %d\n", comms->queueLen ); (XP_UCHAR*)"msg queue len: %d\n", comms->queueLen );
stream_catString( stream, buf ); stream_catString( stream, buf );
XP_U16 indx = 0;
for ( elem = comms->msgQueueHead; !!elem; elem = elem->next ) { for ( elem = comms->msgQueueHead; !!elem; elem = elem->next ) {
XP_SNPRINTF( buf, sizeof(buf), XP_SNPRINTF( buf, sizeof(buf),
" - channelNo=%.4X; msgID=" XP_LD "; len=%d\n", "%d: - channelNo=%.4X; msgID=" XP_LD "; len=%d\n",
elem->channelNo, elem->msgID, elem->len ); indx++, elem->channelNo, elem->msgID, elem->len );
stream_catString( stream, buf ); stream_catString( stream, buf );
} }
for ( rec = comms->recs; !!rec; rec = rec->next ) { for ( rec = comms->recs; !!rec; rec = rec->next ) {
XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf),
(XP_UCHAR*)" Stats for channel: %.4X\n", (XP_UCHAR*)"Stats for channel %.4X\n",
rec->channelNo ); rec->channelNo );
stream_catString( stream, buf ); stream_catString( stream, buf );
XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf),
(XP_UCHAR*)"Last msg sent: " XP_LD "\n", (XP_UCHAR*)" Last msg sent: " XP_LD "; ",
rec->nextMsgID ); rec->nextMsgID );
stream_catString( stream, buf ); stream_catString( stream, buf );
XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf),
(XP_UCHAR*)"Last msg received: %d\n", (XP_UCHAR*)"last msg received: %d\n",
rec->lastMsgRcd ); rec->lastMsgRcd );
stream_catString( stream, buf ); stream_catString( stream, buf );
} }