From f6d7eed84d6aede937c585f1567343e68ac89ae4 Mon Sep 17 00:00:00 2001 From: Eric House Date: Wed, 20 May 2020 13:58:53 -0700 Subject: [PATCH] add mqtt as a new transport This is meant to replace the relay eventually, but for now it's a new option, like BT or SMS, to be chosen. Protocol is handled in common/ code for the first time, meaning that linux and android interact without the need to keep two platforms in sync. Linux uses lib-mosquitto, and Android uses eclipse's Paho client (the generic java version, not the one that uses four-year-old Service patterns and so crashes for SDK >= 26.) --- xwords4/android/app/build.gradle | 9 + .../android/app/src/main/AndroidManifest.xml | 3 + .../eehouse/android/xw4/BoardDelegate.java | 28 +- .../eehouse/android/xw4/CommsTransport.java | 3 + .../android/xw4/ConnStatusHandler.java | 7 + .../android/xw4/ConnViaViewLayout.java | 7 + .../java/org/eehouse/android/xw4/DBUtils.java | 23 +- .../android/xw4/DevIDInviteDelegate.java | 298 ++++++++ .../org/eehouse/android/xw4/DlgDelegate.java | 1 + .../org/eehouse/android/xw4/GameUtils.java | 8 +- .../android/xw4/GamesListDelegate.java | 46 +- .../android/xw4/InviteChoicesAlert.java | 3 + .../android/xw4/MQTTInviteActivity.java | 34 + .../android/xw4/MQTTInviteDelegate.java | 69 ++ .../org/eehouse/android/xw4/MQTTUtils.java | 432 +++++++++++ .../org/eehouse/android/xw4/MultiMsgSink.java | 8 + .../org/eehouse/android/xw4/MultiService.java | 1 + .../org/eehouse/android/xw4/NFCUtils.java | 30 +- .../eehouse/android/xw4/NetLaunchInfo.java | 147 ++-- .../eehouse/android/xw4/PrefsDelegate.java | 8 + .../android/xw4/RelayInviteDelegate.java | 673 +----------------- .../org/eehouse/android/xw4/RequestCode.java | 1 + .../java/org/eehouse/android/xw4/XWApp.java | 6 + .../java/org/eehouse/android/xw4/XWPrefs.java | 17 +- .../eehouse/android/xw4/jni/CommsAddrRec.java | 46 +- .../eehouse/android/xw4/jni/DUtilCtxt.java | 40 +- .../eehouse/android/xw4/jni/GameSummary.java | 32 +- .../org/eehouse/android/xw4/jni/XwJNI.java | 37 +- .../app/src/main/res/values/common_rsrc.xml | 10 + .../app/src/main/res/values/strings.xml | 12 +- .../android/app/src/main/res/xml/xwprefs.xml | 20 + xwords4/android/jni/Android.mk | 1 + xwords4/android/jni/andutils.c | 86 ++- xwords4/android/jni/andutils.h | 7 +- xwords4/android/jni/drawwrapper.c | 52 +- xwords4/android/jni/utilwrapper.c | 68 +- xwords4/android/jni/xportwrapper.c | 21 - xwords4/android/jni/xwjni.c | 201 ++++-- xwords4/common/board.h | 3 - xwords4/common/comms.c | 33 +- xwords4/common/comms.h | 47 +- xwords4/common/commstyp.h | 75 ++ xwords4/common/comtypes.h | 41 ++ xwords4/common/device.c | 116 ++- xwords4/common/device.h | 12 +- xwords4/common/dutil.h | 20 +- xwords4/common/model.h | 31 - xwords4/common/nli.c | 33 +- xwords4/common/nli.h | 51 +- xwords4/common/nlityp.h | 65 ++ xwords4/common/strutils.c | 27 + xwords4/common/strutils.h | 4 + xwords4/linux/Makefile | 2 + xwords4/linux/curgamlistwin.c | 10 +- xwords4/linux/cursesboard.c | 37 +- xwords4/linux/cursesmain.c | 29 +- xwords4/linux/cursesmain.h | 5 + xwords4/linux/gamesdb.c | 18 +- xwords4/linux/gtkboard.c | 29 +- xwords4/linux/gtkconnsdlg.c | 15 +- xwords4/linux/gtkinvit.c | 41 +- xwords4/linux/gtkinvit.h | 3 +- xwords4/linux/gtkmain.c | 58 +- xwords4/linux/gtkmain.h | 5 +- xwords4/linux/lindutil.c | 55 ++ xwords4/linux/linuxmain.c | 79 +- xwords4/linux/main.h | 14 +- xwords4/linux/mqttcon.c | 275 +++++++ xwords4/linux/mqttcon.h | 38 + xwords4/linux/relaycon.h | 2 +- xwords4/linux/scripts/discon_ok2.py | 31 + xwords4/relay/scripts/mqtt-showinplay.py | 65 ++ 72 files changed, 2766 insertions(+), 1098 deletions(-) create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevIDInviteDelegate.java create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteActivity.java create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteDelegate.java create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTUtils.java create mode 100644 xwords4/common/commstyp.h create mode 100644 xwords4/common/nlityp.h create mode 100644 xwords4/linux/mqttcon.c create mode 100644 xwords4/linux/mqttcon.h create mode 100755 xwords4/relay/scripts/mqtt-showinplay.py diff --git a/xwords4/android/app/build.gradle b/xwords4/android/app/build.gradle index cbfb4afd9..b624e918e 100644 --- a/xwords4/android/app/build.gradle +++ b/xwords4/android/app/build.gradle @@ -37,6 +37,9 @@ if ( FABRIC_API_KEY && hasProperty('useCrashlytics') ) { // rm-for-fdroid repositories { google() maven { url 'https://maven.fabric.io/public' } // rm-for-fdroid + maven { + url "https://repo.eclipse.org/content/repositories/paho-releases/" + } } android { @@ -87,6 +90,7 @@ android { buildConfigField "boolean", "LOG_LIFECYLE", "false" buildConfigField "String", "KEY_FCMID", "\"FBMService_fcmid\"" buildConfigField "boolean", "ATTACH_SUPPORTED", "false" + buildConfigField "boolean", "OFFER_MQTT", "false" } xw4NoSMS { @@ -119,6 +123,7 @@ android { externalNativeBuild.ndkBuild.cFlags += ['-DVARIANT_xw4fdroid'] externalNativeBuild.ndkBuild.arguments += ['XW_BT_UUID=' + XW_UUID] } + xw4d { dimension "variant" buildConfigField "String", "DB_NAME", "\"xwddb\"" @@ -133,6 +138,7 @@ android { buildConfigField "String", "KEY_FCMID", "\"FBMService_fcmid1\"" buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\"" resValue "string", "nfc_aid", "$NFC_AID_XW4d" + buildConfigField "boolean", "OFFER_MQTT", "true" externalNativeBuild.ndkBuild.cFlags += ['-DVARIANT_xw4d'] externalNativeBuild.ndkBuild.arguments += ['XW_BT_UUID=' + XWD_UUID] } @@ -150,6 +156,7 @@ android { buildConfigField "boolean", "REPORT_LOCKS", "true" buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\"" resValue "string", "nfc_aid", "$NFC_AID_XW4d" + buildConfigField "boolean", "OFFER_MQTT", "true" externalNativeBuild.ndkBuild.cFlags += ['-DVARIANT_xw4dNoSMS'] externalNativeBuild.ndkBuild.arguments += ['XW_BT_UUID=' + XWD_UUID] } @@ -331,6 +338,8 @@ dependencies { implementation 'com.google.firebase:firebase-core:16.0.6' // rm-for-fdroid implementation 'com.github.eehouse:nbsproxy:v0.2.2' + + implementation "org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.+" } task mkImages(type: Exec) { diff --git a/xwords4/android/app/src/main/AndroidManifest.xml b/xwords4/android/app/src/main/AndroidManifest.xml index b53bde654..da420190f 100644 --- a/xwords4/android/app/src/main/AndroidManifest.xml +++ b/xwords4/android/app/src/main/AndroidManifest.xml @@ -106,6 +106,9 @@ + m_devIDRecs; + private Activity m_activity; + private boolean m_immobileConfirmed; // WTF is this? + + abstract String getRecsKey(); + abstract String getMeDevID(); + + public DevIDInviteDelegate( Delegator delegator, Bundle savedInstanceState ) + { + super( delegator, savedInstanceState ); + m_activity = delegator.getActivity(); + } + + static class DevIDRec implements InviterItem, Serializable { + public String m_devID; + public String m_opponent; + public int m_nPlayers; + + public DevIDRec( String opponent, String devID ) + { + m_devID = devID; + m_nPlayers = 1; + m_opponent = opponent; + } + + public String getDev() { return m_devID; } + + public boolean equals( InviterItem item ) + { + return item != null + && ((DevIDRec)item).m_devID == m_devID; + } + } + + void saveAndRebuild() + { + DBUtils.setSerializableFor( m_activity, getRecsKey(), m_devIDRecs ); + rebuildList( false ); + } + + void rebuildList( boolean checkIfAll ) + { + Collections.sort( m_devIDRecs, new Comparator() { + public int compare( DevIDRec rec1, DevIDRec rec2 ) { + return rec1.m_opponent.compareTo(rec2.m_opponent); + } + }); + + addSelf(); + updateList( m_devIDRecs ); + tryEnable(); + } + + @Override + protected void init( Bundle savedInstanceState ) + { + super.init( savedInstanceState ); + + String msg = getString( R.string.button_invite ); + msg = getQuantityString( R.plurals.invite_relay_desc_fmt, m_nMissing, + m_nMissing, msg ); + super.init( msg, R.string.empty_relay_inviter ); + addButtonBar( R.layout.relay_buttons, BUTTONIDS ); + + getSavedState(); + rebuildList( true ); + } + + @Override + protected void onBarButtonClicked( int id ) + { + switch( id ) { + case R.id.button_relay_add: + Utils.notImpl( getActivity() ); + break; + case R.id.manual_add_button: + showDialogFragment( DlgID.GET_NUMBER ); + break; + case R.id.button_clear: + int count = getChecked().size(); + String msg = getQuantityString( R.plurals.confirm_clear_relay_fmt, + count, count ); + makeConfirmThenBuilder( msg, Action.CLEAR_ACTION ).show(); + break; + case R.id.button_edit: + Object obj = getChecked().iterator().next(); + Log.d( TAG, "passing %s", obj ); + showDialogFragment( DlgID.GET_NUMBER, obj ); + break; + } + } + + @Override + protected Dialog makeDialog( DBAlert alert, Object[] params ) + { + Dialog dialog = null; + DialogInterface.OnClickListener lstnr; + switch( alert.getDlgID() ) { + case GET_NUMBER: { + final DevIDRec curRec = + 1 <= params.length && params[0] instanceof String + ? getHasID((String)params[0]) : null; + Log.d( TAG, "curRec: %s", curRec ); + final View getNumView = inflate( R.layout.get_relay ); + final EditText numField = (EditText) + getNumView.findViewById( R.id.num_field ); + final EditText nameField = (EditText) + getNumView.findViewById( R.id.name_field ); + if ( null != curRec ) { + numField.setText( curRec.m_devID ); + nameField.setText( curRec.m_opponent ); + } + lstnr = new DialogInterface.OnClickListener() { + public void onClick( DialogInterface dlg, int item ) { + String number = numField.getText().toString(); + if ( null != number && 0 < number.length() ) { + String name = nameField.getText().toString(); + if ( curRec != null ) { + curRec.m_opponent = name; + curRec.m_devID = number; + } else { + DevIDRec rec = new DevIDRec( name, number ); + m_devIDRecs.add( rec ); + clearChecked(); + onItemChecked( rec, true ); + } + saveAndRebuild(); + } + } + }; + dialog = makeAlertBuilder() + .setTitle( R.string.get_sms_title ) + .setView( getNumView ) + .setPositiveButton( android.R.string.ok, lstnr ) + .setNegativeButton( android.R.string.cancel, null ) + .create(); + } + break; + default: + dialog = super.makeDialog( alert, params ); + break; + } + return dialog; + } + + @Override + protected void onChildAdded( View child, InviterItem data ) + { + DevIDRec rec = (DevIDRec)data; + ((TwoStrsItem)child).setStrings( rec.m_opponent, rec.m_devID ); + } + + @Override + protected void tryEnable() + { + super.tryEnable(); + + Button button = (Button)findViewById( R.id.button_clear ); + if ( null != button ) { // may not be there yet + button.setEnabled( 0 < getChecked().size() ); + } + button = (Button)findViewById( R.id.button_edit ); + if ( null != button ) { + button.setEnabled( 1 == getChecked().size() ); + } + } + + // DlgDelegate.DlgClickNotify interface + @Override + public boolean onPosButton( Action action, Object[] params ) + { + boolean handled = true; + switch( action ) { + case CLEAR_ACTION: + clearSelectedImpl(); + break; + case USE_IMMOBILE_ACTION: + m_immobileConfirmed = true; + break; + default: + handled = super.onPosButton( action, params ); + break; + } + return handled; + } + + @Override + public boolean onDismissed( Action action, Object[] params ) + { + boolean handled = true; + if ( Action.USE_IMMOBILE_ACTION == action && m_immobileConfirmed ) { + makeConfirmThenBuilder( R.string.warn_unlimited, + Action.POST_WARNING_ACTION ) + .setPosButton( R.string.button_yes ) + .show(); + } else { + handled = false; + } + return handled; + } + + void addSelf() + { + boolean hasSelf = false; + String me = getMeDevID(); + for ( DevIDRec rec : m_devIDRecs ) { + if ( rec.m_devID.equals( me ) ) { + hasSelf = true; + break; + } + } + if ( !hasSelf ) { + DevIDRec rec = new DevIDRec( "self", me ); + m_devIDRecs.add( rec ); + } + } + + void getSavedState() + { + m_devIDRecs = (ArrayList)DBUtils.getSerializableFor( m_activity, getRecsKey() ); + if ( null == m_devIDRecs ) { + m_devIDRecs = new ArrayList<>(); + } + } + + void clearSelectedImpl() + { + Set checked = getChecked(); + for ( Iterator iter = m_devIDRecs.iterator(); iter.hasNext(); ) { + if ( checked.contains( iter.next().getDev() ) ) { + iter.remove(); + } + } + clearChecked(); + saveAndRebuild(); + } + + DevIDRec getHasID( String id ) + { + DevIDRec result = null; + for ( DevIDRec rec : m_devIDRecs ) { + if ( rec.m_devID.equals(id) ) { + result = rec; + break; + } + } + return result; + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java index a692573f6..8fc7267ac 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java @@ -300,6 +300,7 @@ public class DlgDelegate { SMS_DATA, // classic NBS-based data sms EMAIL, NFC, BLUETOOTH, CLIPBOARD, RELAY, WIFIDIRECT, SMS_USER, // just launch the SMS app, as with email + MQTT, }; boolean onPosButton( Action action, Object... params ); boolean onNegButton( Action action, Object... params ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java index aaee1ece1..5d9a24c02 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameUtils.java @@ -735,9 +735,10 @@ public class GameUtils { if ( null != message ) { Intent intent = new Intent(); intent.setAction( Intent.ACTION_SEND ); - String subject = + String subject = null != nli.room ? LocUtils.getString( activity, R.string.invite_subject_fmt, - nli.room ); + nli.room ) + : LocUtils.getString( activity, R.string.invite_subject ); intent.putExtra( Intent.EXTRA_SUBJECT, subject ); intent.putExtra( Intent.EXTRA_TEXT, Html.fromHtml(message) ); @@ -1323,6 +1324,9 @@ public class GameUtils { case COMMS_CONN_P2P: WiDirService.gameDied( addr.p2p_addr, gameID ); break; + case COMMS_CONN_MQTT: + MQTTUtils.gameDied( addr.mqtt_devID, gameID ); + break; } } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java index 94fad0055..85fa80846 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GamesListDelegate.java @@ -100,6 +100,9 @@ public class GamesListDelegate extends ListDelegateBase private static final String REMATCH_RELAYID_EXTRA = "rm_relayid"; private static final String REMATCH_P2PADDR_EXTRA = "rm_p2pma"; + private static final String INVITE_ACTION = "org.eehouse.action_invite"; + private static final String INVITE_DATA = "data_invite"; + private static final String ALERT_MSG = "alert_msg"; private static final String WITH_EMAIL = "with_email"; @@ -2417,12 +2420,13 @@ public class GamesListDelegate extends ListDelegateBase String p2pMacAddress = extras.getString( REMATCH_P2PADDR_EXTRA ); String dict = extras.getString( REMATCH_DICT_EXTRA ); int lang = extras.getInt( REMATCH_LANG_EXTRA, -1 ); + String mqttDevID = extras.getString( GameSummary.EXTRA_REMATCH_MQTT ); String json = extras.getString( REMATCH_PREFS_EXTRA ); newid = GameUtils.makeNewMultiGame( m_activity, groupID, dict, lang, json, addrs, gameName ); DBUtils.addRematchInfo( m_activity, newid, btAddr, phone, - relayID, p2pMacAddress ); + relayID, p2pMacAddress, mqttDevID ); } launchGame( newid ); } @@ -2446,10 +2450,10 @@ public class GamesListDelegate extends ListDelegateBase return handled; } - private boolean tryNFCIntent( Intent intent ) + private boolean tryInviteIntent( Intent intent ) { boolean result = false; - byte[] data = NFCUtils.getFromIntent( intent ); + byte[] data = getFromIntent( intent ); if ( null != data ) { NetLaunchInfo nli = NetLaunchInfo.makeFrom( m_activity, data ); if ( null != nli && nli.isValid() ) { @@ -2681,7 +2685,7 @@ public class GamesListDelegate extends ListDelegateBase || startHasGameID( intent ) || startRematch( intent ) || tryAlert( intent ) - || tryNFCIntent( intent ) + || tryInviteIntent( intent ) ; Log.d( TAG, "tryStartsFromIntent() => handled: %b", handled ); } @@ -2895,7 +2899,7 @@ public class GamesListDelegate extends ListDelegateBase CommsConnTypeSet addrTypes, String btAddr, String phone, String relayID, String p2pMacAddress, - String newName ) + String mqttDevID, String newName ) { Intent intent = null; boolean isSolo = gi.serverRole == CurGameInfo.DeviceRole.SERVER_STANDALONE; @@ -2926,6 +2930,9 @@ public class GamesListDelegate extends ListDelegateBase Assert.assertTrue( addrTypes.contains( CommsConnType.COMMS_CONN_P2P ) ); intent.putExtra( REMATCH_P2PADDR_EXTRA, p2pMacAddress ); } + if ( null != mqttDevID ) { + intent.putExtra( GameSummary.EXTRA_REMATCH_MQTT, mqttDevID ); + } } return intent; } @@ -2944,15 +2951,40 @@ public class GamesListDelegate extends ListDelegateBase ; } - public static void postNFCInvite( Context context, byte[] data ) + public static void postReceivedInvite( Context context, byte[] data ) { Intent intent = makeSelfIntent( context ) .addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ) ; - NFCUtils.populateIntent( context, intent, data ); + populateInviteIntent( context, intent, data ); context.startActivity( intent ); } + private static void populateInviteIntent( Context context, Intent intent, + byte[] data ) + { + NetLaunchInfo nli = NetLaunchInfo.makeFrom( context, data ); + if ( null != nli ) { + intent.setAction( INVITE_ACTION ) + .putExtra( INVITE_DATA, data ); + } else { + Assert.failDbg(); + } + } + + private byte[] getFromIntent( Intent intent ) + { + byte[] result = null; + + String action = intent.getAction(); + if ( INVITE_ACTION.equals( action ) ) { + result = intent.getByteArrayExtra( INVITE_DATA ); + } + + // Log.d( TAG, "getFromIntent() => %s", result ); + return result; + } + public static void openGame( Context context, Uri data ) { Intent intent = makeSelfIntent( context ) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/InviteChoicesAlert.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/InviteChoicesAlert.java index b9a532bfb..a1a72c9bb 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/InviteChoicesAlert.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/InviteChoicesAlert.java @@ -68,6 +68,9 @@ public class InviteChoicesAlert extends DlgDelegateAlert { if ( BuildConfig.DEBUG || !BuildConfig.IS_TAGGED_BUILD ) { add( items, means, R.string.invite_choice_relay, InviteMeans.RELAY ); } + if ( BuildConfig.OFFER_MQTT ) { + add( items, means, R.string.invite_choice_mqtt, InviteMeans.MQTT ); + } if ( WiDirWrapper.enabled() ) { add( items, means, R.string.invite_choice_p2p, InviteMeans.WIFIDIRECT ); } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteActivity.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteActivity.java new file mode 100644 index 000000000..2ea07405f --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteActivity.java @@ -0,0 +1,34 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2015 - 2020 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.os.Bundle; + +public class MQTTInviteActivity extends InviteActivity { + private MQTTInviteDelegate m_dlgt; + + @Override + protected void onCreate( Bundle savedInstanceState ) + { + m_dlgt = new MQTTInviteDelegate( this, savedInstanceState ); + super.onCreate( savedInstanceState, m_dlgt ); + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteDelegate.java new file mode 100644 index 000000000..2c63b9f39 --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTInviteDelegate.java @@ -0,0 +1,69 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2012 - 2020 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.Intent; +import android.os.Bundle; + +import org.eehouse.android.xw4.DBUtils.SentInvitesInfo; +import org.eehouse.android.xw4.jni.XwJNI; + +public class MQTTInviteDelegate extends DevIDInviteDelegate { + private static final String TAG = MQTTInviteDelegate.class.getSimpleName(); + private static final String RECS_KEY = TAG + "/recs"; + private static final boolean MQTTINVITE_SUPPORTED + = BuildConfig.DEBUG || !BuildConfig.IS_TAGGED_BUILD; + + private String m_devIDStr; + + public static void launchForResult( Activity activity, int nMissing, + SentInvitesInfo info, + RequestCode requestCode ) + { + if ( MQTTINVITE_SUPPORTED ) { + Intent intent = + InviteDelegate.makeIntent( activity, MQTTInviteActivity.class, + nMissing, info ); + activity.startActivityForResult( intent, requestCode.ordinal() ); + } + } + + public MQTTInviteDelegate( Delegator delegator, Bundle savedInstanceState ) + { + super( delegator, savedInstanceState ); + } + + @Override + String getRecsKey() + { + return RECS_KEY; + } + + @Override + String getMeDevID() + { + if ( null == m_devIDStr ) { + m_devIDStr = XwJNI.dvc_getMQTTDevID(null); + } + return m_devIDStr; + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTUtils.java new file mode 100644 index 000000000..ddf484f9d --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MQTTUtils.java @@ -0,0 +1,432 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2009 - 2020 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.content.Context; + +import org.eclipse.paho.client.mqttv3.IMqttActionListener; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.IMqttToken; +import org.eclipse.paho.client.mqttv3.MqttAsyncClient; +import org.eclipse.paho.client.mqttv3.MqttCallbackExtended; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicReference; + +import org.eehouse.android.xw4.jni.CommsAddrRec; +import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; +import org.eehouse.android.xw4.jni.XwJNI; +import org.eehouse.android.xw4.loc.LocUtils; + +public class MQTTUtils extends Thread implements IMqttActionListener, MqttCallbackExtended { + private static final String TAG = MQTTUtils.class.getSimpleName(); + private static AtomicReference sInstance = new AtomicReference<>(); + + private MqttAsyncClient mClient; + private long mPauseTime = 0L; + private String mDevID; + private String mTopic; + private Context mContext; + private MsgThread mMsgThread; + private LinkedBlockingQueue mOutboundQueue = new LinkedBlockingQueue<>(); + private boolean mShouldExit = false; + + public static void init( Context context ) + { + Log.d( TAG, "init(OFFER_MQTT:%b)", BuildConfig.OFFER_MQTT ); + getOrStart( context ); + } + + public static void onResume( Context context ) + { + Log.d( TAG, "onResume()" ); + getOrStart( context ); + } + + static void onConfigChanged( Context context ) + { + MQTTUtils instance = sInstance.get(); + if ( null != instance ) { + clearInstance( instance ); + } + } + + private static MQTTUtils getOrStart( Context context ) + { + MQTTUtils result = null; + if ( BuildConfig.OFFER_MQTT ) { + result = sInstance.get(); + if ( null == result ) { + try { + result = new MQTTUtils(context); + setInstance( result ); + result.start(); + } catch ( MqttException me ) { + result = null; + } + } + } + return result; + } + + private static class MessagePair { + byte[] mPacket; + String mTopic; + MessagePair( String topic, byte[] packet ) { + mPacket = packet; + mTopic = topic; + } + } + + @Override + public void run() + { + setup(); + while ( !mShouldExit ) { + try { + // this thread can be fed before the connection is + // established. Wait for that before removing packets from the + // queue. + if ( !mClient.isConnected() ) { + Log.d( TAG, "not connected; sleeping..." ); + Thread.sleep(500); + continue; + } + MessagePair pair = mOutboundQueue.take(); + MqttMessage message = new MqttMessage( pair.mPacket ); + mClient.publish( pair.mTopic, message ); + } catch ( MqttException me ) { + me.printStackTrace(); + break; + } catch ( InterruptedException ie ) { + ie.printStackTrace(); + break; + } + } + clearInstance(); + } + + private void enqueue( String topic, byte[] packet ) + { + mOutboundQueue.add( new MessagePair( topic, packet ) ); + } + + private static void setInstance( MQTTUtils instance ) + { + MQTTUtils oldInstance = sInstance.getAndSet(instance); + if ( null != oldInstance ) { + oldInstance.disconnect(); + } + } + + private static void clearInstance( MQTTUtils curInstance ) + { + MQTTUtils oldInstance = sInstance.getAndSet(null); + if ( curInstance == oldInstance ) { + oldInstance.disconnect(); + } + // if ( sResumed ) { + // Log.d( TAG, "clearInstance(); looks like I could start another!!" ); + // } + } + + public static void onPause() + { + // Log.d( TAG, "onPause()" ); + // MQTTUtils instance = sInstance.get(); + // if ( null != instance ) { + // instance.setPaused(true); + // } + // DbgUtils.assertOnUIThread(); + // // sResumed = false; + } + + private MQTTUtils( Context context ) throws MqttException + { + Log.d( TAG, "%H.()", this ); + mContext = context; + String[] topic = {null}; + mDevID = XwJNI.dvc_getMQTTDevID( topic ); + mTopic = topic[0]; + mMsgThread = new MsgThread(); + + String host = XWPrefs.getPrefsString( context, R.string.key_mqtt_host ); + Log.d( TAG, "host: %s", host ); + int port = XWPrefs.getPrefsInt( context, R.string.key_mqtt_port, 1883 ); + String url = String.format( java.util.Locale.US, "tcp://%s:%d", host, port ); + Log.d( TAG, "using url: %s", url ); + mClient = new MqttAsyncClient( url, mDevID, new MemoryPersistence() ); + mClient.setCallback( this ); + } + + private void setup() + { + Log.d( TAG, "setup()" ); + MqttConnectOptions mqttConnectOptions = new MqttConnectOptions(); + mqttConnectOptions.setAutomaticReconnect(true); + mqttConnectOptions.setCleanSession(false); + final int qos = XWPrefs.getPrefsInt( mContext, R.string.key_mqtt_qos, 2 ); + + try { + mClient.connect( mqttConnectOptions, null, new IMqttActionListener() { + @Override + public void onSuccess( IMqttToken asyncActionToken ) { + Log.d( TAG, "onSuccess()" ); + try { + mClient.subscribe( mTopic, qos, null, MQTTUtils.this ); + Log.d( TAG, "subscribed to %s", mTopic ); + mMsgThread.start(); + } catch ( MqttException ex ) { + ex.printStackTrace(); + } catch ( Exception ex ) { + ex.printStackTrace(); + clearInstance(); + } + } + @Override + public void onFailure(IMqttToken asyncActionToken, Throwable exception) { + Log.d( TAG, "onFailure(%s, %s)", asyncActionToken, exception ); + ConnStatusHandler.updateStatus( mContext, null, + CommsConnType.COMMS_CONN_MQTT, + false ); + clearInstance(); + } + } ); + } catch ( MqttException ex ) { + ex.printStackTrace(); + } catch ( java.lang.IllegalStateException ise ) { + ise.printStackTrace(); + } catch ( Exception ise ) { + ise.printStackTrace(); + clearInstance(); + } + } + + // private void setPaused( boolean paused ) + // { + // if ( paused ) { + // if ( 0 == mPauseTime ) { + // mPauseTime = System.currentTimeMillis(); + // Log.d( TAG, "setPaused() called for first time" ); + // } + // } else { + // long diff = System.currentTimeMillis() - mPauseTime; + // Log.d( TAG, "unpausing after %d seconds", diff/1000); + // mPauseTime = 0; + // } + // } + + private void disconnect() + { + if ( 0 == mPauseTime ) { + Log.d( TAG, "disconnect()" ); + } else { + long diff = System.currentTimeMillis() - mPauseTime; + Log.d( TAG, "disconnect() called %d seconds after app paused", diff/1000 ); + } + try { + mShouldExit = true; + mClient.unsubscribe( mDevID ); + mClient.disconnect(); + Log.d( TAG, "disconnect() succeeded" ); + mMsgThread.interrupt(); + } catch (MqttException ex){ + ex.printStackTrace(); + } catch (Exception ex){ + ex.printStackTrace(); + clearInstance(); + } + } + + private void clearInstance() { clearInstance( this ); } + + public static void inviteRemote( Context context, String invitee, NetLaunchInfo nli ) + { + String[] topic = {invitee}; + byte[] packet = XwJNI.dvc_makeMQTTInvite( nli, topic ); + addToSendQueue( context, topic[0], packet ); + } + + private static void notifyNotHere( Context context, String addressee, int gameID ) + { + String[] topic = {addressee}; + byte[] packet = XwJNI.dvc_makeMQTTNoSuchGame( gameID, topic ); + addToSendQueue( context, topic[0], packet ); + } + + public static int send( Context context, String addressee, int gameID, byte[] buf ) + { + Log.d( TAG, "send(to:%s, len: %d)", addressee, buf.length ); + String[] topic = {addressee}; + byte[] packet = XwJNI.dvc_makeMQTTMessage( gameID, buf, topic ); + addToSendQueue( context, topic[0], packet ); + return buf.length; + } + + private static void addToSendQueue( Context context, String topic, byte[] packet ) + { + MQTTUtils instance = getOrStart( context ); + if ( null != instance ) { + instance.enqueue( topic, packet ); + } + } + + public static void gameDied( String devID, int gameID ) + { + Log.e( TAG, "gameDied() not handled" ); + } + + @Override + public void connectComplete(boolean reconnect, String serverURI) + { + Log.d( TAG, "%H.connectComplete(reconnect=%b, serverURI=%s)", this, + reconnect, serverURI ); + } + + @Override + public void connectionLost( Throwable cause ) + { + Log.d( TAG, "%H.connectionLost(%s)", this, cause ); + clearInstance(); + } + + @Override + public void messageArrived( String topic, MqttMessage message) throws Exception + { + Log.d( TAG, "messageArrived(topic=%s, message=%s)", topic, message ); + Assert.assertTrueNR( topic.equals(mTopic) ); + mMsgThread.add( message.getPayload() ); + ConnStatusHandler + .updateStatusIn( mContext, CommsConnType.COMMS_CONN_MQTT, true ); + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) + { + Log.d( TAG, "deliveryComplete(token=%s)", token ); + ConnStatusHandler + .updateStatusOut( mContext, CommsConnType.COMMS_CONN_MQTT, true ); + } + + @Override + public void onSuccess(IMqttToken asyncActionToken) + { + Log.d( TAG, "onSuccess(%s)", asyncActionToken ); + } + + @Override + public void onFailure(IMqttToken asyncActionToken, Throwable exception) + { + Log.d( TAG, "onFailure(%s, %s)", asyncActionToken, exception ); + } + + private class MsgThread extends Thread { + private LinkedBlockingQueue mQueue = new LinkedBlockingQueue<>(); + + void add( byte[] msg ) { + mQueue.add( msg ); + } + + @Override + public void run() + { + for ( ; ; ) { + try { + byte[] packet = mQueue.take(); + XwJNI.dvc_parseMQTTPacket( packet ); + } catch ( InterruptedException ie ) { + // Assert.failDbg(); + break; + } + } + Log.d( TAG, "%H.run() exiting", this ); + } + } + + public static void handleMessage( Context context, CommsAddrRec from, + int gameID, byte[] data ) + { + Log.d( TAG, "handleMessage(gameID=%d): got message", gameID ); + MQTTServiceHelper helper = new MQTTServiceHelper( context, from ); + long[] rowids = DBUtils.getRowIDsFor( context, gameID ); + Log.d( TAG, "got %d rows for gameID %d", rowids == null ? 0 : rowids.length, gameID ); + if ( 0 == rowids.length ) { + notifyNotHere( context, from.mqtt_devID, gameID ); + } else { + for ( long rowid : rowids ) { + MQTTMsgSink sink = new MQTTMsgSink( context, rowid ); + helper.receiveMessage( rowid, sink, data ); + } + } + } + + public static void handleGameGone( Context context, CommsAddrRec from, int gameID ) + { + new MQTTServiceHelper( context, from ) + .postEvent( MultiService.MultiEvent.MESSAGE_NOGAME, gameID ); + } + + private static class MQTTServiceHelper extends XWServiceHelper { + private CommsAddrRec mReturnAddr; + private Context mContext; + + MQTTServiceHelper( Context context, CommsAddrRec from ) + { + super( context ); + mContext = context; + mReturnAddr = from; + } + + @Override + protected MultiMsgSink getSink( long rowid ) + { + Context context = getContext(); + return new MQTTMsgSink( context, rowid ); + } + + @Override + void postNotification( String device, int gameID, long rowid ) + { + Assert.failDbg(); + // Context context = getContext(); + // String body = LocUtils.getString( mContext, R.string.new_relay_body ); + // GameUtils.postInvitedNotification( mContext, gameID, body, rowid ); + } + + private void receiveMessage( long rowid, MQTTMsgSink sink, byte[] msg ) + { + Log.d( TAG, "receiveMessage(rowid=%d, len=%d)", rowid, msg.length ); + receiveMessage( rowid, sink, msg, mReturnAddr ); + } + } + + private static class MQTTMsgSink extends MultiMsgSink { + MQTTMsgSink( Context context, long rowid ) + { + super( context, rowid ); + } + } + +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiMsgSink.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiMsgSink.java index 7b08a5b7c..ee0dd19a9 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiMsgSink.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiMsgSink.java @@ -84,6 +84,11 @@ public class MultiMsgSink implements TransportProcs { return NFCUtils.addMsgFor( buf, gameID ); } + int sendViaMQTT( String addressee, byte[] buf, int gameID ) + { + return MQTTUtils.send( m_context, addressee, gameID, buf ); + } + public int numSent() { return m_sentSet.size(); @@ -114,6 +119,9 @@ public class MultiMsgSink implements TransportProcs { case COMMS_CONN_NFC: nSent = sendViaNFC( buf, gameID ); break; + case COMMS_CONN_MQTT: + nSent = sendViaMQTT( addr.mqtt_devID, buf, gameID ); + break; default: Assert.failDbg(); break; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java index 1b33a4cf1..3f2d63379 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/MultiService.java @@ -50,6 +50,7 @@ public class MultiService { public static final String BT_NAME = "BT_NAME"; public static final String BT_ADDRESS = "BT_ADDRESS"; public static final String P2P_MAC_ADDRESS = "P2P_MAC_ADDRESS"; + public static final String MQTT_DEVID = "MQTT_DEVID"; private static final String NLI_DATA = "nli"; public static final String DUPEMODE = "du"; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java index 6d675a00e..4c2983127 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java @@ -59,9 +59,6 @@ public class NFCUtils { private static final String TAG = NFCUtils.class.getSimpleName(); private static final boolean USE_BIGINTEGER = true; - private static final String NFC_TO_SELF_ACTION = "org.eehouse.nfc_to_self"; - private static final String NFC_TO_SELF_DATA = "nfc_data"; - static final byte VERSION_1 = (byte)0x01; private static final byte MESSAGE = 0x01; @@ -91,31 +88,6 @@ public class NFCUtils { return s_nfcAvail; } - public static byte[] getFromIntent( Intent intent ) - { - byte[] result = null; - - String action = intent.getAction(); - if ( NFC_TO_SELF_ACTION.equals( action ) ) { - result = intent.getByteArrayExtra( NFC_TO_SELF_DATA ); - } - - // Log.d( TAG, "getFromIntent() => %s", result ); - return result; - } - - public static void populateIntent( Context context, Intent intent, - byte[] data ) - { - NetLaunchInfo nli = NetLaunchInfo.makeFrom( context, data ); - if ( null != nli ) { - intent.setAction( NFC_TO_SELF_ACTION ) - .putExtra( NFC_TO_SELF_DATA, data ); - } else { - Assert.failDbg(); - } - } - public static Dialog makeEnableNFCDialog( final Activity activity ) { DialogInterface.OnClickListener lstnr @@ -415,7 +387,7 @@ public class NFCUtils { } break; case INVITE: - GamesListDelegate.postNFCInvite( context, body ); + GamesListDelegate.postReceivedInvite( context, body ); break; case REPLY: switch( body[0] ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java index 36cec5391..f0ec2c399 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NetLaunchInfo.java @@ -1,6 +1,6 @@ /* -*- compile-command: "find-and-gradle.sh inXw4Deb"; -*- */ /* - * Copyright 2009 - 2018 by Eric House (xwords@eehouse.org). All rights + * Copyright 2009 - 2020 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or @@ -45,6 +45,7 @@ import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; import org.eehouse.android.xw4.jni.CommsAddrRec; import org.eehouse.android.xw4.jni.CurGameInfo; import org.eehouse.android.xw4.jni.GameSummary; +import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.loc.LocUtils; public class NetLaunchInfo implements Serializable { @@ -65,8 +66,11 @@ public class NetLaunchInfo implements Serializable { private static final String FORCECHANNEL_KEY = "fc"; private static final String NAME_KEY = "nm"; private static final String P2P_MAC_KEY = "p2"; + private static final String MQTT_DEVID_KEY = "r2id"; private static final String DUPMODE_KEY = "du"; + private static final int EMPTY_SET = new CommsConnTypeSet().toInt(); + protected String gameName; protected String dict; protected int lang; @@ -83,17 +87,18 @@ public class NetLaunchInfo implements Serializable { protected boolean isGSM; protected int osVers; - private int _conTypes; // for syncing with the c version only!! + // MQTT + protected String mqttDevID; + + private int _conTypes; private int gameID = 0; - private CommsConnTypeSet m_addrs; private boolean m_valid; private String inviteID; private boolean dupeMode; public NetLaunchInfo() { - m_addrs = new CommsConnTypeSet(); - // Give it a random number. It may be overwritten, but so what. + _conTypes = EMPTY_SET; inviteID = GameUtils.formatGameID( Utils.nextRandomInt() ); } @@ -117,8 +122,9 @@ public class NetLaunchInfo implements Serializable { btName = bundle.getString( MultiService.BT_NAME ); btAddress = bundle.getString( MultiService.BT_ADDRESS ); p2pMacAddress = bundle.getString( MultiService.P2P_MAC_ADDRESS ); + mqttDevID = bundle.getString( MultiService.MQTT_DEVID ); - m_addrs = new CommsConnTypeSet( bundle.getInt( ADDRS_KEY ) ); + _conTypes = bundle.getInt( ADDRS_KEY ); Utils.testSerialization( this ); } @@ -183,14 +189,15 @@ public class NetLaunchInfo implements Serializable { String val = data.getQueryParameter( ADDRS_KEY ); boolean hasAddrs = null != val; if ( hasAddrs ) { - m_addrs = new CommsConnTypeSet( Integer.decode( val ) ); + _conTypes = Integer.decode( val ); } else { - m_addrs = new CommsConnTypeSet(); + _conTypes = EMPTY_SET; } List supported = CommsConnTypeSet.getSupported( context ); + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes ); for ( CommsConnType typ : supported ) { - if ( hasAddrs && !m_addrs.contains( typ ) ) { + if ( hasAddrs && !addrs.contains( typ ) ) { continue; } boolean doAdd; @@ -222,15 +229,20 @@ public class NetLaunchInfo implements Serializable { case COMMS_CONN_NFC: doAdd = true; break; + case COMMS_CONN_MQTT: + mqttDevID = data.getQueryParameter( MQTT_DEVID_KEY ); + doAdd = !hasAddrs && null != mqttDevID; + break; default: doAdd = false; Log.d( TAG, "unexpected type: %s", typ ); Assert.failDbg(); } if ( doAdd ) { - m_addrs.add( typ ); + addrs.add( typ ); } } + _conTypes = addrs.toInt(); removeUnsupported( supported ); @@ -307,6 +319,9 @@ public class NetLaunchInfo implements Serializable { case COMMS_CONN_NFC: addNFCInfo(); break; + case COMMS_CONN_MQTT: + addMQTTInfo(); + break; default: Assert.failDbg(); break; @@ -316,12 +331,14 @@ public class NetLaunchInfo implements Serializable { public boolean contains( CommsConnType typ ) { - return m_addrs.contains( typ ); + return new CommsConnTypeSet( _conTypes ).contains( typ ); } public void removeAddress( CommsConnType typ ) { - m_addrs.remove( typ ); + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes ); + addrs.remove( typ ); + _conTypes = addrs.toInt(); } public String inviteID() @@ -365,12 +382,12 @@ public class NetLaunchInfo implements Serializable { bundle.putString( MultiService.BT_ADDRESS, btAddress ); bundle.putString( MultiService.P2P_MAC_ADDRESS, p2pMacAddress ); bundle.putInt( MultiService.FORCECHANNEL, forceChannel ); + bundle.putString( MultiService.MQTT_DEVID, mqttDevID ); if ( dupeMode ) { bundle.putBoolean( MultiService.DUPEMODE, true ); } - int flags = m_addrs.toInt(); - bundle.putInt( ADDRS_KEY, flags ); + bundle.putInt( ADDRS_KEY, _conTypes ); } @Override @@ -391,14 +408,14 @@ public class NetLaunchInfo implements Serializable { && TextUtils.equals( room, other.room ) && TextUtils.equals( btName, other.btName ) && TextUtils.equals( btAddress, other.btAddress ) + && TextUtils.equals( mqttDevID, other.mqttDevID ) && TextUtils.equals( p2pMacAddress, other.p2pMacAddress ) && TextUtils.equals( phone, other.phone ) && isGSM == other. isGSM && osVers == other.osVers && _conTypes == other._conTypes && gameID == other.gameID - && ((null == m_addrs ? (null == other.m_addrs) - : m_addrs.equals(other.m_addrs))) + && _conTypes == other._conTypes && m_valid == other.m_valid && TextUtils.equals( inviteID, other.inviteID ) ; @@ -411,7 +428,7 @@ public class NetLaunchInfo implements Serializable { String result = null; try { JSONObject obj = new JSONObject() - .put( ADDRS_KEY, m_addrs.toInt() ) + .put( ADDRS_KEY, _conTypes ) .put( MultiService.LANG, lang ) .put( MultiService.DICT, dict ) .put( MultiService.GAMENAME, gameName ) @@ -426,23 +443,28 @@ public class NetLaunchInfo implements Serializable { obj.put( MultiService.DUPEMODE, dupeMode ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_RELAY ) ) { + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes ); + if ( addrs.contains( CommsConnType.COMMS_CONN_RELAY ) ) { obj.put( MultiService.ROOM, room ) .put( MultiService.INVITEID, inviteID ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_BT ) ) { + if ( addrs.contains( CommsConnType.COMMS_CONN_BT ) ) { obj.put( MultiService.BT_NAME, btName ) .put( MultiService.BT_ADDRESS, btAddress ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_SMS ) ) { + if ( addrs.contains( CommsConnType.COMMS_CONN_SMS ) ) { obj.put( PHONE_KEY, phone ) .put( GSM_KEY, isGSM ) .put( OSVERS_KEY, osVers ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_P2P ) ) { + if ( addrs.contains( CommsConnType.COMMS_CONN_P2P ) ) { obj.put( P2P_MAC_KEY, p2pMacAddress ); } + + if ( addrs.contains( CommsConnType.COMMS_CONN_MQTT ) ) { + obj.put( MQTT_DEVID_KEY, mqttDevID ); + } result = obj.toString(); } catch ( org.json.JSONException jse ) { @@ -455,7 +477,8 @@ public class NetLaunchInfo implements Serializable { public CommsAddrRec makeAddrRec( Context context ) { CommsAddrRec result = new CommsAddrRec(); - for ( CommsConnType typ : m_addrs.getTypes() ) { + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes ); + for ( CommsConnType typ : addrs.getTypes() ) { result.conTypes.add( typ ); switch( typ ) { case COMMS_CONN_RELAY: @@ -474,6 +497,9 @@ public class NetLaunchInfo implements Serializable { break; case COMMS_CONN_NFC: break; + case COMMS_CONN_MQTT: + result.setMQTTParams( mqttDevID ); + break; default: Assert.failDbg(); break; @@ -490,8 +516,7 @@ public class NetLaunchInfo implements Serializable { int flags = json.optInt(ADDRS_KEY, -1); boolean hasAddrs = -1 != flags; - m_addrs = hasAddrs ? - new CommsConnTypeSet( flags ) : new CommsConnTypeSet(); + _conTypes = hasAddrs ? flags : EMPTY_SET; lang = json.optInt( MultiService.LANG, -1 ); forceChannel = json.optInt( MultiService.FORCECHANNEL, 0 ); @@ -504,8 +529,9 @@ public class NetLaunchInfo implements Serializable { gameID = json.optInt( MultiService.GAMEID, 0 ); // Try each type + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes ); for ( CommsConnType typ : supported ) { - if ( hasAddrs && !m_addrs.contains( typ ) ) { + if ( hasAddrs && !addrs.contains( typ ) ) { continue; } boolean doAdd; @@ -533,16 +559,21 @@ public class NetLaunchInfo implements Serializable { case COMMS_CONN_NFC: doAdd = NFCUtils.nfcAvail( context )[0]; break; + case COMMS_CONN_MQTT: + mqttDevID = json.optString( MQTT_DEVID_KEY ); + doAdd = BuildConfig.OFFER_MQTT && null != mqttDevID; + break; default: doAdd = false; Assert.failDbg(); } if ( doAdd ) { - m_addrs.add( typ ); + addrs.add( typ ); } } removeUnsupported( supported ); + _conTypes = addrs.toInt(); calcValid(); } @@ -554,7 +585,6 @@ public class NetLaunchInfo implements Serializable { public Uri makeLaunchUri( Context context ) { - int addrs = m_addrs.toInt(); String host = LocUtils.getString( context, R.string.invite_host ); host = NetUtils.forceHost( host ); Uri.Builder ub = new Uri.Builder() @@ -566,7 +596,7 @@ public class NetLaunchInfo implements Serializable { appendInt( ub, HEREPLAYERS_KEY, nPlayersH ); appendInt( ub, GID_KEY, gameID() ); appendInt( ub, FORCECHANNEL_KEY, forceChannel ); - appendInt( ub, ADDRS_KEY, addrs ); + appendInt( ub, ADDRS_KEY, _conTypes ); ub.appendQueryParameter( NAME_KEY, gameName ); if ( dupeMode ) { appendInt( ub, DUPMODE_KEY, 1 ); @@ -576,22 +606,26 @@ public class NetLaunchInfo implements Serializable { ub.appendQueryParameter( WORDLIST_KEY, dict ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_RELAY ) ) { + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes ); + if ( addrs.contains( CommsConnType.COMMS_CONN_RELAY ) ) { ub.appendQueryParameter( ROOM_KEY, room ); ub.appendQueryParameter( ID_KEY, inviteID ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_BT ) ) { + if ( addrs.contains( CommsConnType.COMMS_CONN_BT ) ) { ub.appendQueryParameter( BTADDR_KEY, btAddress ); ub.appendQueryParameter( BTNAME_KEY, btName ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_SMS ) ) { + if ( addrs.contains( CommsConnType.COMMS_CONN_SMS ) ) { ub.appendQueryParameter( PHONE_KEY, phone ); appendInt( ub, GSM_KEY, (isGSM? 1 : 0) ); appendInt( ub, OSVERS_KEY, osVers ); } - if ( m_addrs.contains( CommsConnType.COMMS_CONN_P2P ) ) { + if ( addrs.contains( CommsConnType.COMMS_CONN_P2P ) ) { ub.appendQueryParameter( P2P_MAC_KEY, p2pMacAddress ); } + if ( addrs.contains( CommsConnType.COMMS_CONN_MQTT ) ) { + ub.appendQueryParameter( MQTT_DEVID_KEY, mqttDevID ); + } Uri result = ub.build(); if ( BuildConfig.DEBUG ) { // Test... @@ -603,11 +637,18 @@ public class NetLaunchInfo implements Serializable { return result; } + private void add( CommsConnType typ ) + { + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes ); + addrs.add( typ ); + _conTypes = addrs.toInt(); + } + public void addRelayInfo( String aRoom, String inviteID ) { room = aRoom; inviteID = inviteID; - m_addrs.add( CommsConnType.COMMS_CONN_RELAY ); + add( CommsConnType.COMMS_CONN_RELAY ); } public void addBTInfo() @@ -616,7 +657,7 @@ public class NetLaunchInfo implements Serializable { if ( null != got ) { btName = got[0]; btAddress = got[1]; - m_addrs.add( CommsConnType.COMMS_CONN_BT ); + add( CommsConnType.COMMS_CONN_BT ); } else { Log.w( TAG, "addBTInfo(): no BT info available" ); } @@ -631,19 +672,25 @@ public class NetLaunchInfo implements Serializable { osVers = Integer.valueOf( android.os.Build.VERSION.SDK ); - m_addrs.add( CommsConnType.COMMS_CONN_SMS ); + add( CommsConnType.COMMS_CONN_SMS ); } } public void addP2PInfo( Context context ) { p2pMacAddress = WiDirService.getMyMacAddress( context ); - m_addrs.add( CommsConnType.COMMS_CONN_P2P ); + add( CommsConnType.COMMS_CONN_P2P ); } public void addNFCInfo() { - m_addrs.add( CommsConnType.COMMS_CONN_NFC ); + add( CommsConnType.COMMS_CONN_NFC ); + } + + public void addMQTTInfo() + { + add( CommsConnType.COMMS_CONN_MQTT ); + mqttDevID = XwJNI.dvc_getMQTTDevID( null ); } public boolean isValid() @@ -686,16 +733,6 @@ public class NetLaunchInfo implements Serializable { Assert.failDbg(); } - public void freezeAddrs() - { - _conTypes = m_addrs.toInt(); - } - - public void unfreezeAddrs() - { - m_addrs = new CommsConnTypeSet( _conTypes, true ); - } - private boolean hasCommon() { return null != dict @@ -706,20 +743,25 @@ public class NetLaunchInfo implements Serializable { private void removeUnsupported( List supported ) { - for ( Iterator iter = m_addrs.iterator(); + CommsConnTypeSet addrs = new CommsConnTypeSet( _conTypes );// , true ); + for ( Iterator iter = addrs.iterator(); iter.hasNext(); ) { - if ( !supported.contains( iter.next() ) ) { + CommsConnType typ = iter.next(); + if ( !supported.contains( typ ) ) { + Log.d( TAG, "removing %s", typ ); iter.remove(); } } + _conTypes = addrs.toInt(); } private void calcValid() { - boolean valid = hasCommon() && null != m_addrs; + boolean valid = hasCommon(); // Log.d( TAG, "calcValid(%s); valid (so far): %b", this, valid ); if ( valid ) { - for ( Iterator iter = m_addrs.iterator(); + for ( Iterator iter + = new CommsConnTypeSet( _conTypes ).iterator(); valid && iter.hasNext(); ) { CommsConnType typ = iter.next(); switch ( typ ) { @@ -732,6 +774,9 @@ public class NetLaunchInfo implements Serializable { case COMMS_CONN_SMS: valid = null != phone && 0 < osVers; break; + case COMMS_CONN_MQTT: + valid = null != mqttDevID; + break; } } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java index 04d2b0359..9abb91d43 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java @@ -62,6 +62,9 @@ public class PrefsDelegate extends DelegateBase R.string.key_disable_nag_solo, R.string.key_disable_relay, R.string.key_force_tablet, + R.string.key_mqtt_host, + R.string.key_mqtt_port, + R.string.key_mqtt_qos, }; private static Map s_keysHash = null; @@ -236,6 +239,11 @@ public class PrefsDelegate extends DelegateBase case R.string.key_force_tablet: makeOkOnlyBuilder( R.string.after_restart ).show(); break; + case R.string.key_mqtt_host: + case R.string.key_mqtt_port: + case R.string.key_mqtt_qos: + MQTTUtils.onConfigChanged( m_activity ); + break; default: Assert.failDbg(); break; diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayInviteDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayInviteDelegate.java index 1a42739f9..e98074161 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayInviteDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayInviteDelegate.java @@ -1,6 +1,6 @@ /* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ /* - * Copyright 2012 - 2015 by Eric House (xwords@eehouse.org). All rights + * Copyright 2012 - 2020 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or @@ -21,59 +21,19 @@ package org.eehouse.android.xw4; import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; -import android.text.method.DigitsKeyListener; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.CompoundButton; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.Spinner; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -import org.json.JSONArray; -import org.json.JSONObject; import org.eehouse.android.xw4.DBUtils.SentInvitesInfo; -import org.eehouse.android.xw4.DlgDelegate.Action; -public class RelayInviteDelegate extends InviteDelegate { +public class RelayInviteDelegate extends DevIDInviteDelegate { private static final String TAG = RelayInviteDelegate.class.getSimpleName(); - private static final String RECS_KEY = "TAG" + "/recs"; + private static final String RECS_KEY = TAG + "/recs"; private static final boolean RELAYINVITE_SUPPORTED = BuildConfig.DEBUG || !BuildConfig.IS_TAGGED_BUILD; - private static int[] BUTTONIDS = { - R.id.button_relay_add, - R.id.manual_add_button, - R.id.button_clear, - R.id.button_edit, - }; - - private static final String SAVE_NAME = "SAVE_NAME"; - private static final String SAVE_NUMBER = "SAVE_NUMBER"; - - private ArrayList m_devIDRecs; private boolean m_immobileConfirmed; - private Activity m_activity; - private String m_devIDStr; + private String mRelayDevIDStr; public static void launchForResult( Activity activity, int nMissing, SentInvitesInfo info, @@ -90,633 +50,20 @@ public class RelayInviteDelegate extends InviteDelegate { public RelayInviteDelegate( Delegator delegator, Bundle savedInstanceState ) { super( delegator, savedInstanceState ); - m_activity = delegator.getActivity(); } @Override - protected void init( Bundle savedInstanceState ) + String getRecsKey() { - if ( RELAYINVITE_SUPPORTED ) { - super.init( savedInstanceState ); - - String msg = getString( R.string.button_invite ); - msg = getQuantityString( R.plurals.invite_relay_desc_fmt, m_nMissing, - m_nMissing, msg ); - super.init( msg, R.string.empty_relay_inviter ); - addButtonBar( R.layout.relay_buttons, BUTTONIDS ); - - m_devIDStr = String.format( "%d", DevID.getRelayDevIDInt(m_activity) ); - - // getBundledData( savedInstanceState ); - - // m_addButton = (ImageButton)findViewById( R.id.manual_add_button ); - // m_addButton.setOnClickListener( new View.OnClickListener() { - // public void onClick( View view ) - // { - // showDialog( DlgID.GET_NUMBER ); - // } - // } ); - - // if ( XWPrefs.getRelayInviteToSelfEnabled( m_activity ) ) { - // ImageButton addMe = (ImageButton)findViewById( R.id.add_self_button ); - // addMe.setVisibility( View.VISIBLE ); - // addMe.setOnClickListener( new View.OnClickListener() { - // public void onClick( View view ) { - // int devIDInt = DevID.getRelayDevIDInt( m_activity ); - // String devID = String.format( "%d", devIDInt ); - // DevIDRec rec = new DevIDRec( "self", devID ); - // addChecked( rec ); - // saveAndRebuild(); - // } - // } ); - // } - - getSavedState(); - rebuildList( true ); - } + return RECS_KEY; } @Override - protected void onBarButtonClicked( int id ) + String getMeDevID() { - if ( RELAYINVITE_SUPPORTED ) { - switch( id ) { - case R.id.button_relay_add: - Utils.notImpl( m_activity ); - break; - case R.id.manual_add_button: - showDialogFragment( DlgID.GET_NUMBER ); - break; - case R.id.button_clear: - int count = getChecked().size(); - String msg = getQuantityString( R.plurals.confirm_clear_relay_fmt, - count, count ); - makeConfirmThenBuilder( msg, Action.CLEAR_ACTION ).show(); - break; - case R.id.button_edit: - showDialogFragment( DlgID.GET_NUMBER, getChecked().iterator().next() ); - break; - } + if ( null == mRelayDevIDStr ) { + mRelayDevIDStr = String.format( "%d", DevID.getRelayDevIDInt(getActivity()) ); } + return mRelayDevIDStr; } - - // protected void onSaveInstanceState( Bundle outState ) - // { - // outState.putString( SAVE_NAME, m_pendingName ); - // outState.putString( SAVE_NUMBER, m_pendingNumber ); - // } - - // private void getBundledData( Bundle bundle ) - // { - // if ( null != bundle ) { - // m_pendingName = bundle.getString( SAVE_NAME ); - // m_pendingNumber = bundle.getString( SAVE_NUMBER ); - // } - // } - - // protected void onActivityResult( int requestCode, int resultCode, - // Intent data ) - // { - // // super.onActivityResult( requestCode, resultCode, data ); - // if ( Activity.RESULT_CANCELED != resultCode && data != null ) { - // switch (requestCode) { - // case GET_CONTACT: - // addPhoneNumbers( data ); - // break; - // } - // } - // } - - @Override - protected Dialog makeDialog( DBAlert alert, Object[] params ) - { - Dialog dialog = null; - if ( RELAYINVITE_SUPPORTED ) { - DialogInterface.OnClickListener lstnr; - switch( alert.getDlgID() ) { - case GET_NUMBER: { - final DevIDRec curRec = - 1 <= params.length && params[0] instanceof DevIDRec - ? (DevIDRec)params[0] : null; - final View getNumView = inflate( R.layout.get_relay ); - final EditText numField = (EditText) - getNumView.findViewById( R.id.num_field ); - numField.setKeyListener(DigitsKeyListener.getInstance()); - final EditText nameField = (EditText) - getNumView.findViewById( R.id.name_field ); - if ( null != curRec ) { - numField.setText( curRec.m_devID ); - nameField.setText( curRec.m_opponent ); - } - lstnr = new DialogInterface.OnClickListener() { - public void onClick( DialogInterface dlg, int item ) { - String number = numField.getText().toString(); - if ( null != number && 0 < number.length() ) { - String name = nameField.getText().toString(); - if ( curRec != null ) { - curRec.m_opponent = name; - curRec.m_devID = number; - } else { - DevIDRec rec = new DevIDRec( name, number ); - m_devIDRecs.add( rec ); - clearChecked(); - onItemChecked( rec, true ); - } - saveAndRebuild(); - } - } - }; - dialog = makeAlertBuilder() - .setTitle( R.string.get_sms_title ) - .setView( getNumView ) - .setPositiveButton( android.R.string.ok, lstnr ) - .setNegativeButton( android.R.string.cancel, null ) - .create(); - } - break; - default: - dialog = super.makeDialog( alert, params ); - break; - } - } - return dialog; - } - - // protected Dialog onCreateDialog( int id ) - // { - // Dialog dialog = super.onCreateDialog( id ); - // if ( null == dialog ) { - // DialogInterface.OnClickListener lstnr; - // DlgID dlgID = DlgID.values()[id]; - // switch( dlgID ) { - // case GET_NUMBER: - // final GameNamer namerView = - // (GameNamer)inflate( R.layout.rename_game ); - // namerView.setLabel( R.string.get_relay_number ); - // namerView.setKeyListener(DialerKeyListener.getInstance()); - // lstnr = new DialogInterface.OnClickListener() { - // public void onClick( DialogInterface dlg, int item ) { - // String devID = namerView.getName(); - // if ( 0 < devID.length() ) { - // DevIDRec rec = new DevIDRec( devID ); - // addChecked( new DevIDRec( devID ) ); - // saveAndRebuild(); - // } - // } - // }; - // dialog = makeAlertBuilder() - // .setNegativeButton( android.R.string.cancel, null ) - // .setPositiveButton( android.R.string.ok, lstnr ) - // .setView( namerView ) - // .create(); - // break; - // } - // setRemoveOnDismiss( dialog, dlgID ); - // } - // return dialog; - // } - - @Override - protected void onChildAdded( View child, InviterItem data ) - { - DevIDRec rec = (DevIDRec)data; - ((TwoStrsItem)child).setStrings( rec.m_opponent, rec.m_devID ); - } - - // We want to present user with list of previous opponents and devices. We - // can easily get list of relayIDs. The relay, if reachable, can convert - // that to a (likely shorter) list of devices. Then for each deviceID, - // open the newest game with a relayID mapping to it and get the name of - // the opponent? - - // protected void scan() - // { - // long[][] rowIDss = new long[1][]; - // String[] relayIDs = DBUtils.getRelayIDs( m_activity, rowIDss ); - - // if ( null != relayIDs && 0 < relayIDs.length ) { - // new ListOpponentsTask( m_activity, relayIDs, rowIDss[0] ).execute(); - // } - - // // Intent intent = new Intent( Intent.ACTION_PICK, - // // ContactsContract.Contacts.CONTENT_URI ); - // // intent.setType( Phone.CONTENT_TYPE ); - // // startActivityForResult( intent, GET_CONTACT ); - // } - - // @Override - // protected void clearSelected( Integer[] selected ) - // { - // makeConfirmThenBuilder( R.string.confirm_clear_relay, Action.CLEAR_ACTION ) - // .show(); - // } - - // protected void listSelected( String[][] devsP, int[][] countsP ) - // { - // // int count = m_adapter.getCount(); - // // String[] result = new String[countChecks()]; - // // int[] counts = new int[result.length]; - - // // int index = 0; - // // Iterator iter = m_devIDRecs.iterator(); - // // for ( int ii = 0; iter.hasNext(); ++ii ) { - // // DevIDRec rec = iter.next(); - // // if ( rec.m_isChecked ) { - // // counts[index] = rec.m_nPlayers; - // // result[index] = ((SMSListItem)m_adapter.getItem(ii)).getNumber(); - // // index++; - // // } - // // } - // // devsP[0] = result; - // // if ( null != countsP ) { - // // countsP[0] = counts; - // // } - // } - - @Override - protected void tryEnable() - { - if ( RELAYINVITE_SUPPORTED ) { - super.tryEnable(); - - Button button = (Button)findViewById( R.id.button_clear ); - if ( null != button ) { // may not be there yet - button.setEnabled( 0 < getChecked().size() ); - } - button = (Button)findViewById( R.id.button_edit ); - if ( null != button ) { - button.setEnabled( 1 == getChecked().size() ); - } - } - } - - // DlgDelegate.DlgClickNotify interface - @Override - public boolean onPosButton( Action action, Object[] params ) - { - boolean handled = true; - if ( RELAYINVITE_SUPPORTED ) { - switch( action ) { - case CLEAR_ACTION: - clearSelectedImpl(); - break; - case USE_IMMOBILE_ACTION: - m_immobileConfirmed = true; - break; - default: - handled = super.onPosButton( action, params ); - break; - } - } - return handled; - } - - @Override - public boolean onDismissed( Action action, Object[] params ) - { - boolean handled = true; - if ( RELAYINVITE_SUPPORTED ) { - if ( Action.USE_IMMOBILE_ACTION == action && m_immobileConfirmed ) { - makeConfirmThenBuilder( R.string.warn_unlimited, - Action.POST_WARNING_ACTION ) - .setPosButton( R.string.button_yes ) - .show(); - } else { - handled = false; - } - } - return handled; - } - - // private int countChecks() - // { - // int count = 0; - // if ( null != m_devIDRecs ) { - // Iterator iter = m_devIDRecs.iterator(); - // while ( iter.hasNext() ) { - // if ( iter.next().m_isChecked ) { - // ++count; - // } - // } - // } - // return count; - // } - - // private void addPhoneNumbers( Intent intent ) - // { - // Uri data = intent.getData(); - // Cursor cursor = m_activity - // .managedQuery( data, - // new String[] { Phone.DISPLAY_NAME, - // Phone.NUMBER, - // Phone.TYPE }, - // null, null, null ); - // // Have seen a crash reporting - // // "android.database.StaleDataException: Attempted to access a - // // cursor after it has been closed." when the query takes a - // // long time to return. Be safe. - // if ( null != cursor && !cursor.isClosed() ) { - // if ( cursor.moveToFirst() ) { - // String name = - // cursor.getString( cursor. - // getColumnIndex( Phone.DISPLAY_NAME)); - // String number = - // cursor.getString( cursor. - // getColumnIndex( Phone.NUMBER ) ); - - // int type = cursor.getInt( cursor. - // getColumnIndex( Phone.TYPE ) ); - // // m_pendingName = name; - // // m_pendingNumber = number; - // if ( Phone.TYPE_MOBILE == type ) { - // showConfirmThen( R.string.warn_unlimited, - // R.string.button_yes, - // Action.POST_WARNING_ACTION ); - // } else { - // m_immobileConfirmed = false; - // String msg = getString( R.string.warn_nomobile_fmt, - // number, name ); - // showConfirmThen( msg, R.string.button_yes, - // Action.USE_IMMOBILE_ACTION ); - // } - // } - // } - // } // addPhoneNumbers - - private void addSelf() - { - boolean hasSelf = false; - for ( DevIDRec rec : m_devIDRecs ) { - if ( rec.m_devID.equals( m_devIDStr ) ) { - hasSelf = true; - break; - } - } - if ( !hasSelf ) { - DevIDRec rec = new DevIDRec( "me", m_devIDStr ); - m_devIDRecs.add( rec ); - } - } - - private void rebuildList( boolean checkIfAll ) - { - Collections.sort( m_devIDRecs, new Comparator() { - public int compare( DevIDRec rec1, DevIDRec rec2 ) { - return rec1.m_opponent.compareTo(rec2.m_opponent); - } - }); - - addSelf(); - updateList( m_devIDRecs ); - tryEnable(); - } - - private void getSavedState() - { - m_devIDRecs = (ArrayList)DBUtils.getSerializableFor( m_activity, RECS_KEY ); - if ( null == m_devIDRecs ) { - m_devIDRecs = new ArrayList<>(); - } - } - - private void saveAndRebuild() - { - DBUtils.setSerializableFor( m_activity, RECS_KEY, m_devIDRecs ); - rebuildList( false ); - } - - // private void addChecked( DevIDRec rec ) - // { - // if ( m_nMissing <= countChecks() ) { - // Iterator iter = m_devIDRecs.iterator(); - // while ( iter.hasNext() ) { - // iter.next().m_isChecked = false; - // } - // } - - // rec.m_isChecked = true; - // m_devIDRecs.add( rec ); - // } - - private void clearSelectedImpl() - { - Set checked = getChecked(); - for ( Iterator iter = m_devIDRecs.iterator(); iter.hasNext(); ) { - if ( checked.contains( iter.next().getDev() ) ) { - iter.remove(); - } - } - clearChecked(); - saveAndRebuild(); - } - - private static class DevIDRec implements InviterItem, Serializable { - public String m_devID; - public String m_opponent; - public int m_nPlayers; - // public DevIDRec( String name, String devID ) - // { - // this( name, devID, false ); - // } - // public DevIDRec( String devID ) - // { - // this( null, devID, false ); - // } - - public DevIDRec( String opponent, String devID ) - { - m_devID = devID; - m_nPlayers = 1; - m_opponent = opponent; - } - - public String getDev() { return m_devID; } - - public boolean equals( InviterItem item ) - { - return item != null - && ((DevIDRec)item).m_devID == m_devID; - } - } - - // private class RelayDevsAdapter extends XWListAdapter { - // private SMSListItem[] m_items; - - // public RelayDevsAdapter() - // { - // super( m_devIDRecs.size() ); - // m_items = new SMSListItem[m_devIDRecs.size()]; - // } - - // public Object getItem( final int position ) - // { - // // For some reason I can't cache items to be returned. - // // Checking/unchecking breaks for some but not all items, - // // with some relation to whether they were scrolled into - // // view. So build them anew each time (but still cache - // // for by-index access.) - - // SMSListItem item = - // (SMSListItem)inflate( R.layout.smsinviter_item ); - // // item.setChecked( m_devIDRecs.get(position).m_isChecked ); - - // CompoundButton.OnCheckedChangeListener lstnr = - // new CompoundButton.OnCheckedChangeListener() { - // public void onCheckedChanged( CompoundButton bv, - // boolean isChecked ) { - // m_devIDRecs.get(position).m_isChecked = isChecked; - // tryEnable(); - // } - // }; - // item.setOnCheckedChangeListener( lstnr ); - // final DevIDRec rec = m_devIDRecs.get( position ); - // item.setContents( rec.m_opponent, rec.m_devID ); - // m_items[position] = item; - - // // Set up spinner - // Assert.assertTrue( 1 == rec.m_nPlayers ); - // if ( XWPrefs.getCanInviteMulti( m_activity ) && 1 < m_nMissing ) { - // Spinner spinner = (Spinner) - // item.findViewById(R.id.nperdev_spinner); - // ArrayAdapter adapter = - // new ArrayAdapter<>( m_activity, android.R.layout - // .simple_spinner_item ); - // for ( int ii = 1; ii <= m_nMissing; ++ii ) { - // String str = getQuantityString( R.plurals.nplayers_fmt, ii, ii ); - // adapter.add( str ); - // } - // spinner.setAdapter( adapter ); - // spinner.setVisibility( View.VISIBLE ); - // spinner.setOnItemSelectedListener( new OnItemSelectedListener() { - // public void onItemSelected( AdapterView parent, - // View view, int pos, - // long id ) - // { - // rec.m_nPlayers = 1 + pos; - // tryEnable(); - // } - - // public void onNothingSelected( AdapterView parent ) {} - // } ); - // } - - // return item; - // } - - // public View getView( final int position, View convertView, - // ViewGroup parent ) { - // return (View)getItem( position ); - // } - // } - - // private class ListOpponentsTask extends AsyncTask> { - // private Context m_context; - // private String[] m_relayIDs; - // private long[] m_rowIDs; - - // public ListOpponentsTask( Context context, String[] relayIDs, long[] rowIDs ) { - // m_context = context; - // m_relayIDs = relayIDs; - // m_rowIDs = rowIDs; - // } - - // @Override protected Set doInBackground( Void... unused ) - // { - // Set result = null; - // JSONObject reply = null; - // try { - // startProgress( R.string.fetching_from_relay ); - - // JSONArray ids = new JSONArray(); - // for ( String id : m_relayIDs ) { - // ids.put( id ); - // } - // JSONObject params = new JSONObject(); - // params.put( "relayIDs", ids ); - // params.put( "me", DevID.getRelayDevIDInt( m_activity ) ); - // Log.i( TAG, "sending to server: %s", params.toString() ); - - // HttpURLConnection conn = NetUtils.makeHttpConn( m_context, "opponentIDsFor" ); - // if ( null != conn ) { - // String str = NetUtils.runConn( conn, params ); - // Log.i( TAG, "got json from server: %s", str ); - // reply = new JSONObject( str ); - // } - - // if ( null != reply ) { - // result = new HashSet<>(); - - // setProgressMsg( R.string.processing_games ); - - // JSONArray objs = reply.getJSONArray("devIDs"); - // for ( int ii = 0; ii < objs.length(); ++ii ) { - // JSONObject obj = objs.getJSONObject( ii ); - // Iterator keys = obj.keys(); - // Assert.assertTrue( keys.hasNext() ); - // String key = keys.next(); - // Assert.assertFalse( keys.hasNext() ); - // JSONArray devIDs2 = obj.getJSONArray( key ); - // for ( int jj = 0; jj < devIDs2.length(); ++jj ) { - // result.add( devIDs2.getString(jj) ); - // } - // } - // } - - // } catch ( org.json.JSONException je ) { - // Log.ex( TAG, je ); - // } - - // stopProgress(); - // return result; - // } - - // @Override protected void onPostExecute( Set devIDs ) - // { - // if ( null == devIDs ) { - // Log.w( TAG, "onPostExecute: no results from server?" ); - // } else { - // m_devIDRecs = new ArrayList<>(devIDs.size()); - // Iterator iter = devIDs.iterator(); - // while ( iter.hasNext() ) { - // String devID = iter.next(); - // DevIDRec rec = new DevIDRec( "name", devID ); - // m_devIDRecs.add( rec ); - // } - - // // m_adapter = new RelayDevsAdapter(); - // // setListAdapter( m_adapter ); - // // // m_checked.clear(); - // // tryEnable(); - // } - // } - - // private void startProgress( final int msgID ) - // { - // runOnUiThread( new Runnable() { - // public void run() { - // RelayInviteDelegate.this - // .startProgress( R.string.rel_invite_title, msgID ); - // } - // } ); - // } - - // private void setProgressMsg( final int id ) - // { - // runOnUiThread( new Runnable() { - // public void run() { - // RelayInviteDelegate.this.setProgressMsg( id ); - // } - // } ); - // } - - // private void stopProgress() - // { - // runOnUiThread( new Runnable() { - // public void run() { - // RelayInviteDelegate.this.stopProgress(); - // } - // } ); - // } - - // } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java index 9d515676d..efe655237 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RequestCode.java @@ -30,6 +30,7 @@ public enum RequestCode { SMS_DATA_INVITE_RESULT, RELAY_INVITE_RESULT, P2P_INVITE_RESULT, + MQTT_INVITE_RESULT, // PermUtils PERM_REQUEST, diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java index 77a3f8fb1..b19cfbedd 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWApp.java @@ -101,6 +101,8 @@ public class XWApp extends Application NBSProxy.register( this, mPort, BuildConfig.APPLICATION_ID, this ); DupeModeTimer.init( this ); + + MQTTUtils.init( this ); } @OnLifecycleEvent(ON_ANY) @@ -109,12 +111,16 @@ public class XWApp extends Application Log.d( TAG, "onAny(%s)", event ); switch( event ) { case ON_RESUME: + MQTTUtils.onResume( this ); // Do here what checkForMoves does if ( null != DBUtils.getRelayIDs( this, null ) ) { RelayService.timerFired( this ); } GameUtils.resendAllIf( this, null ); break; + case ON_PAUSE: + MQTTUtils.onPause(); + break; } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java index cb5f1a372..dc2520c80 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java @@ -205,7 +205,19 @@ public class XWPrefs { String key = context.getString( keyID ); SharedPreferences sp = PreferenceManager .getDefaultSharedPreferences( context ); - return sp.getInt( key, defaultValue ); + int result; + try { + result = sp.getInt( key, defaultValue ); + // If it's in a pref, it'll be a string (editable) So will get CCE + } catch ( ClassCastException cce ) { + String asStr = sp.getString( key, String.format( "%d", defaultValue ) ); + try { + result = Integer.parseInt( asStr ); + } catch ( Exception ex ) { + result = defaultValue; + } + } + return result; } public static void setPrefsInt( Context context, int keyID, int newValue ) @@ -521,6 +533,9 @@ public class XWPrefs { if ( RelayService.relayEnabled( context ) ) { result.add( CommsConnType.COMMS_CONN_RELAY ); } + if ( BuildConfig.OFFER_MQTT ) { + result.add( CommsConnType.COMMS_CONN_MQTT ); + } if ( BTService.BTEnabled() ) { result.add( CommsConnType.COMMS_CONN_BT ); } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommsAddrRec.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommsAddrRec.java index 5e1326f7c..65376f8cd 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommsAddrRec.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/CommsAddrRec.java @@ -1,6 +1,6 @@ /* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ /* - * Copyright 2009-2010 by Eric House (xwords@eehouse.org). All + * Copyright 2009-2020 by Eric House (xwords@eehouse.org). All * rights reserved. * * This program is free software; you can redistribute it and/or @@ -54,7 +54,8 @@ public class CommsAddrRec { COMMS_CONN_BT, COMMS_CONN_SMS, COMMS_CONN_P2P, - COMMS_CONN_NFC(false); + COMMS_CONN_NFC(false), + COMMS_CONN_MQTT; private boolean mIsSelectable = true; @@ -82,6 +83,8 @@ public class CommsAddrRec { id = R.string.invite_choice_p2p; break; case COMMS_CONN_NFC: id = R.string.invite_choice_nfc; break; + case COMMS_CONN_MQTT: + id = R.string.invite_choice_mqtt; break; default: Assert.failDbg(); } @@ -101,17 +104,16 @@ public class CommsAddrRec { public CommsConnTypeSet() { this(BIT_VECTOR_MASK); } - public CommsConnTypeSet( int bits, boolean isVector ) + public CommsConnTypeSet( final int inBits ) { - this( bits | BIT_VECTOR_MASK ); - Assert.assertTrue( isVector ); - } - - public CommsConnTypeSet( int bits ) - { - boolean isVector = 0 != (BIT_VECTOR_MASK & bits); - bits &= ~BIT_VECTOR_MASK; + boolean isVector = 0 != (BIT_VECTOR_MASK & inBits); + int bits = inBits & ~BIT_VECTOR_MASK; CommsConnType[] values = CommsConnType.values(); + // Deal with games saved before I added the BIT_VECTOR_MASK back + // in. This should be removable before ship. Or later of course. + if ( !isVector && bits >= values.length ) { + isVector = true; + } if ( isVector ) { for ( CommsConnType value : values ) { int ord = value.ordinal(); @@ -119,8 +121,10 @@ public class CommsAddrRec { add( value ); } } - } else { + } else if ( bits < values.length ) { // don't crash add( values[bits] ); + } else { + Log.e( TAG, ": bad bits value: 0x%x", inBits ); } } @@ -147,6 +151,9 @@ public class CommsAddrRec { { List supported = new ArrayList<>(); supported.add( CommsConnType.COMMS_CONN_RELAY ); + if ( BuildConfig.OFFER_MQTT ) { + supported.add( CommsConnType.COMMS_CONN_MQTT ); + } if ( BTService.BTAvailable() ) { supported.add( CommsConnType.COMMS_CONN_BT ); } @@ -216,9 +223,6 @@ public class CommsAddrRec { private static final CommsConnType[] s_hint = new CommsConnType[0]; } - // The C equivalent of this struct uses a union for the various - // data sets below. So don't assume that any fields will be valid - // except those for the current conType. public CommsConnTypeSet conTypes; // relay case @@ -240,6 +244,9 @@ public class CommsAddrRec { // wifi-direct public String p2p_addr; + // mqtt + public String mqtt_devID; + public CommsAddrRec( CommsConnType cTyp ) { this(); @@ -318,6 +325,12 @@ public class CommsAddrRec { return this; } + public CommsAddrRec setMQTTParams( String devID ) + { + mqtt_devID = devID; + return this; + } + public void populate( Context context, CommsConnTypeSet newTypes ) { for ( CommsConnType typ : newTypes.getTypes() ) { @@ -405,6 +418,9 @@ public class CommsAddrRec { case COMMS_CONN_P2P: p2p_addr = WiDirService.getMyMacAddress( context ); break; + case COMMS_CONN_MQTT: + mqtt_devID = XwJNI.dvc_getMQTTDevID( null ); + break; case COMMS_CONN_NFC: break; default: diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java index 31c4e504b..5a476c2d6 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/DUtilCtxt.java @@ -24,7 +24,10 @@ import android.content.Context; import android.content.Intent; import android.telephony.PhoneNumberUtils; +import java.util.Arrays; + import org.eehouse.android.xw4.Assert; +import org.eehouse.android.xw4.BuildConfig; import org.eehouse.android.xw4.Channels; import org.eehouse.android.xw4.DBUtils; import org.eehouse.android.xw4.DevID; @@ -33,6 +36,8 @@ import org.eehouse.android.xw4.FBMService; import org.eehouse.android.xw4.GameUtils; import org.eehouse.android.xw4.GamesListDelegate; import org.eehouse.android.xw4.Log; +import org.eehouse.android.xw4.MQTTUtils; +import org.eehouse.android.xw4.NetLaunchInfo; import org.eehouse.android.xw4.R; import org.eehouse.android.xw4.Utils; import org.eehouse.android.xw4.XWApp; @@ -253,21 +258,19 @@ public class DUtilCtxt { // Log.d( TAG, "store(key=%s)", key ); if ( null != data ) { DBUtils.setBytesFor( m_context, key, data ); + if ( BuildConfig.DEBUG ) { + byte[] tmp = load( key ); + Assert.assertTrue( Arrays.equals( tmp, data ) ); + } } } public byte[] load( String key ) { - byte[] result = null; - int resultLen = 0; - Log.d( TAG, "load(key=%s)", key ); + byte[] result = DBUtils.getBytesFor( m_context, key ); - result = DBUtils.getBytesFor( m_context, key ); - if ( result != null ) { - resultLen = result.length; - } - - Log.d( TAG, "load(%s) returning %d bytes", key, resultLen ); + // Log.d( TAG, "load(%s) returning %d bytes", key, + // null == result ? 0 : result.length ); return result; } @@ -337,4 +340,23 @@ public class DUtilCtxt { { DupeModeTimer.timerChanged( m_context, gameID, newVal ); } + + public void onInviteReceived( NetLaunchInfo nli ) + { + Log.d( TAG, "onInviteReceived(%s)", nli ); + GamesListDelegate.postReceivedInvite( m_context, nli.asByteArray() ); + } + + public void onMessageReceived( int gameID, CommsAddrRec from, byte[] msg ) + { + Log.d( TAG, "onMessageReceived()" ); + Assert.assertTrueNR( from.contains( CommsAddrRec.CommsConnType.COMMS_CONN_MQTT ) ); + MQTTUtils.handleMessage( m_context, from, gameID, msg ); + } + + public void onGameGoneReceived( int gameID, CommsAddrRec from ) + { + Assert.assertTrueNR( from.contains( CommsAddrRec.CommsConnType.COMMS_CONN_MQTT ) ); + MQTTUtils.handleGameGone( m_context, from, gameID ); + } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java index c51f3acfe..940bc2d99 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/GameSummary.java @@ -50,6 +50,7 @@ public class GameSummary implements Serializable { public static final String EXTRA_REMATCH_PHONE = "rm_phone"; public static final String EXTRA_REMATCH_RELAY = "rm_relay"; public static final String EXTRA_REMATCH_P2P = "rm_p2p"; + public static final String EXTRA_REMATCH_MQTT = "rm_mqtt"; public static final int MSG_FLAGS_NONE = 0; public static final int MSG_FLAGS_TURN = 1; @@ -279,7 +280,8 @@ public class GameSummary implements Serializable { // Otherwise, use BT or SMS if ( null == result ) { if ( conTypes.contains( CommsConnType.COMMS_CONN_BT ) - || ( conTypes.contains( CommsConnType.COMMS_CONN_SMS))){ + || conTypes.contains( CommsConnType.COMMS_CONN_SMS) + || conTypes.contains( CommsConnType.COMMS_CONN_MQTT )) { if ( 0 < missing ) { if ( DeviceRole.SERVER_ISSERVER == serverRole ) { fmtID = R.string.summary_wait_host; @@ -469,21 +471,24 @@ public class GameSummary implements Serializable { m_extras = data; } - public void putStringExtra( String key, String value ) + public GameSummary putStringExtra( String key, String value ) { - String extras = (null == m_extras) ? "{}" : m_extras; - try { - JSONObject asObj = new JSONObject( extras ); - if ( null == value ) { - asObj.remove( key ); - } else { - asObj.put( key, value ); + if ( null != value ) { + String extras = (null == m_extras) ? "{}" : m_extras; + try { + JSONObject asObj = new JSONObject( extras ); + if ( null == value ) { + asObj.remove( key ); + } else { + asObj.put( key, value ); + } + m_extras = asObj.toString(); + } catch( org.json.JSONException ex ) { + Log.ex( TAG, ex ); } - m_extras = asObj.toString(); - } catch( org.json.JSONException ex ) { - Log.ex( TAG, ex ); + Log.i( TAG, "putStringExtra(%s,%s) => %s", key, value, m_extras ); } - Log.i( TAG, "putStringExtra(%s,%s) => %s", key, value, m_extras ); + return this; } public String getStringExtra( String key ) @@ -511,6 +516,7 @@ public class GameSummary implements Serializable { EXTRA_REMATCH_PHONE, EXTRA_REMATCH_RELAY, EXTRA_REMATCH_P2P, + EXTRA_REMATCH_MQTT, }; for ( String key : keys ) { found = null != getStringExtra( key ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java index 4c734dd82..3bac75628 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/jni/XwJNI.java @@ -127,6 +127,32 @@ public class XwJNI { cleanGlobals(); } + public static String dvc_getMQTTDevID( String[] topic ) + { + return dvc_getMQTTDevID( getJNI().m_ptrGlobals, topic ); + } + + public static byte[] dvc_makeMQTTInvite( NetLaunchInfo nli, String[] addrToTopic ) + { + return dvc_makeMQTTInvite( getJNI().m_ptrGlobals, nli, addrToTopic ); + } + + public static byte[] dvc_makeMQTTMessage( int gameID, byte[] buf, + String[] addrToTopic ) + { + return dvc_makeMQTTMessage( getJNI().m_ptrGlobals, gameID, buf, addrToTopic ); + } + + public static byte[] dvc_makeMQTTNoSuchGame( int gameID, String[] addrToTopic ) + { + return dvc_makeMQTTNoSuchGame( getJNI().m_ptrGlobals, gameID, addrToTopic ); + } + + public static void dvc_parseMQTTPacket( byte[] buf ) + { + dvc_parseMQTTPacket( getJNI().m_ptrGlobals, buf ); + } + private static void cleanGlobals() { synchronized( XwJNI.class ) { // let's be safe here @@ -174,7 +200,6 @@ public class XwJNI { public static byte[] nliToStream( NetLaunchInfo nli ) { - nli.freezeAddrs(); return nli_to_stream( getJNI().m_ptrGlobals, nli ); } @@ -182,7 +207,6 @@ public class XwJNI { { NetLaunchInfo nli = new NetLaunchInfo(); nli_from_stream( getJNI().m_ptrGlobals, nli, stream ); - nli.unfreezeAddrs(); return nli; } @@ -533,6 +557,15 @@ public class XwJNI { // Private methods -- called only here private static native long initGlobals( DUtilCtxt dutil, JNIUtils jniu ); + private static native String dvc_getMQTTDevID( long jniState, String[] topic ); + private static native byte[] dvc_makeMQTTInvite( long jniState, NetLaunchInfo nli, + String[] addrToTopic ); + private static native byte[] dvc_makeMQTTMessage( long jniState, int gameID, byte[] buf, + String[] addrToTopic ); + private static native byte[] dvc_makeMQTTNoSuchGame( long jniState, int gameID, + String[] addrToTopic ); + + private static native void dvc_parseMQTTPacket( long jniState, byte[] buf ); private static native void cleanGlobals( long jniState ); private static native byte[] gi_to_stream( long jniState, CurGameInfo gi ); private static native void gi_from_stream( long jniState, CurGameInfo gi, diff --git a/xwords4/android/app/src/main/res/values/common_rsrc.xml b/xwords4/android/app/src/main/res/values/common_rsrc.xml index 9da8bc52a..eebeb0434 100644 --- a/xwords4/android/app/src/main/res/values/common_rsrc.xml +++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml @@ -39,6 +39,9 @@ key_relay_via_http_first key_update_url2 key_relay_url2 + key_mqtt_host + key_mqtt_port + key_mqtt_qos key_update_prerel key_proxy_port key_sms_port @@ -143,6 +146,7 @@ key_notagain_nbsGamesOnUpgrade key_na_comms_bt key_na_comms_p2p + key_na_comms_mqtt key_na_comms_sms key_na_comms_relay key_na_bt_badproto @@ -339,6 +343,12 @@ @string/relay_poll_name_both + + 0 + 1 + 2 + + @string/force_tablet_default @string/force_tablet_phone diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 75d89723c..ffc6aba3b 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -1048,6 +1048,7 @@ + Let\'s play CrossWords Let\'s play CrossWords (room %1$s) - Waiting for connection[s] + Waiting for guest[s] Unconnected @@ -2195,6 +2196,7 @@ For debugging You should never need these… Relay host + MQTT host Use Web APIs first (instead of as fallback for custom protocol) Wordlist download URL @@ -2207,6 +2209,8 @@ Source version id Device ID (on relay) Relay game port + MQTT port + MQTT QOS Relay device port %1$s/%2$s Write games to SD card @@ -2551,4 +2555,10 @@ Pick a tile \"spelling\" \"%1$s\" cannot be spelled with tiles in %2$s. + + + Internet/MQTT + MQTT Invitation + I\'m experimenting with this + as a replacement for the relay. diff --git a/xwords4/android/app/src/main/res/xml/xwprefs.xml b/xwords4/android/app/src/main/res/xml/xwprefs.xml index e3121f213..3e71b9209 100644 --- a/xwords4/android/app/src/main/res/xml/xwprefs.xml +++ b/xwords4/android/app/src/main/res/xml/xwprefs.xml @@ -456,6 +456,26 @@ android:defaultValue="10998" android:numeric="decimal" /> + + + + + name, val ); - /* XP_LOGF( "%s: read int %s of size %d with val %d from offset %d", */ - /* __func__, si->name, si->siz, val, si->offset ); */ + /* XP_LOGFF( "read int %s of size %d with val %d/0x%x from offset %d", */ + /* si->name, si->siz, val, val, si->offset ); */ } } @@ -185,7 +185,7 @@ setBools( JNIEnv* env, jobject jobj, void* cobj, const SetInfo* sis, XP_U16 nSis bool setString( JNIEnv* env, jobject obj, const char* name, const XP_UCHAR* value ) { - // XP_LOGF( "%s(%s)", __func__, name ); + /* XP_LOGFF( "(name=%s, val=%s)", name, value ); */ bool success = false; jclass cls = (*env)->GetObjectClass( env, obj ); jfieldID fid = (*env)->GetFieldID( env, cls, name, "Ljava/lang/String;" ); @@ -226,7 +226,7 @@ void getString( JNIEnv* env, jobject obj, const char* name, XP_UCHAR* buf, int bufLen ) { - // XP_LOGF( "%s(%s)", __func__, name ); + /* XP_LOGFF( "(name=%s, bufLen=%d)", name, bufLen ); */ jclass cls = (*env)->GetObjectClass( env, obj ); XP_ASSERT( !!cls ); jfieldID fid = (*env)->GetFieldID( env, cls, name, "Ljava/lang/String;" ); @@ -509,8 +509,27 @@ setTypeSetFieldIn( JNIEnv* env, const CommsAddrRec* addr, jobject jTarget, deleteLocalRef( env, jtypset ); } +jobject +makeJAddr( JNIEnv* env, const CommsAddrRec* addr ) +{ + jobject jaddr = NULL; + if ( NULL != addr ) { + jclass clazz + = (*env)->FindClass(env, PKG_PATH("jni/CommsAddrRec") ); + XP_ASSERT( !!clazz ); + jmethodID mid = getMethodID( env, clazz, "", "()V" ); + XP_ASSERT( !!mid ); + + jaddr = (*env)->NewObject( env, clazz, mid ); + setJAddrRec( env, jaddr, addr ); + + deleteLocalRef( env, clazz ); + } + return jaddr; +} + /* Copy C object data into Java object */ -void +jobject setJAddrRec( JNIEnv* env, jobject jaddr, const CommsAddrRec* addr ) { XP_ASSERT( !!addr ); @@ -543,10 +562,17 @@ setJAddrRec( JNIEnv* env, jobject jaddr, const CommsAddrRec* addr ) break; case COMMS_CONN_NFC: break; + case COMMS_CONN_MQTT: { + XP_UCHAR buf[32]; + formatMQTTDevID( &addr->u.mqtt.devID, buf, VSIZE(buf) ); + setString( env, jaddr, "mqtt_devID", buf ); + } + break; default: XP_ASSERT(0); } } + return jaddr; } jobject @@ -636,6 +662,12 @@ getJAddrRec( JNIEnv* env, CommsAddrRec* addr, jobject jaddr ) break; case COMMS_CONN_NFC: break; + case COMMS_CONN_MQTT: { + XP_UCHAR buf[32]; + getString( env, jaddr, "mqtt_devID", buf, VSIZE(buf) ); + sscanf( buf, MQTTDevID_FMT, &addr->u.mqtt.devID ); + } + break; default: XP_ASSERT(0); } @@ -723,6 +755,48 @@ jEnumToInt( JNIEnv* env, jobject jenum ) return (*env)->CallIntMethod( env, jenum, mid ); } +static const SetInfo nli_ints[] = { + ARR_MEMBER( NetLaunchInfo, _conTypes ), + ARR_MEMBER( NetLaunchInfo, lang ), + ARR_MEMBER( NetLaunchInfo, forceChannel ), + ARR_MEMBER( NetLaunchInfo, nPlayersT ), + ARR_MEMBER( NetLaunchInfo, nPlayersH ), + ARR_MEMBER( NetLaunchInfo, gameID ), + ARR_MEMBER( NetLaunchInfo, osVers ), +}; + +static const SetInfo nli_bools[] = { + ARR_MEMBER( NetLaunchInfo, isGSM ), + ARR_MEMBER( NetLaunchInfo, remotesAreRobots ), +}; + +static const SetInfo nli_strs[] = { + ARR_MEMBER( NetLaunchInfo, dict ), + ARR_MEMBER( NetLaunchInfo, gameName ), + ARR_MEMBER( NetLaunchInfo, room ), + ARR_MEMBER( NetLaunchInfo, btName ), + ARR_MEMBER( NetLaunchInfo, btAddress ), + ARR_MEMBER( NetLaunchInfo, phone ), + ARR_MEMBER( NetLaunchInfo, inviteID ), + ARR_MEMBER( NetLaunchInfo, mqttDevID ), +}; + +void +loadNLI( JNIEnv* env, NetLaunchInfo* nli, jobject jnli ) +{ + getInts( env, (void*)nli, jnli, AANDS(nli_ints) ); + getBools( env, (void*)nli, jnli, AANDS(nli_bools) ); + getStrings( env, (void*)nli, jnli, AANDS(nli_strs) ); +} + +void +setNLI( JNIEnv* env, jobject jnli, const NetLaunchInfo* nli ) +{ + setInts( env, jnli, (void*)nli, AANDS(nli_ints) ); + setBools( env, jnli, (void*)nli, AANDS(nli_bools) ); + setStrings( env, jnli, (void*)nli, AANDS(nli_strs) ); +} + XWStreamCtxt* and_empty_stream( MPFORMAL AndGameGlobals* globals ) { @@ -785,7 +859,7 @@ passToJava( const char* tag, const char* msg ) releaseEnvFromGlobals( env ); } else { - RAW_LOG( "env is NULL; dropping" ); + // RAW_LOG( "env is NULL; dropping" ); } } diff --git a/xwords4/android/jni/andutils.h b/xwords4/android/jni/andutils.h index 9d1d57811..935d6a89a 100644 --- a/xwords4/android/jni/andutils.h +++ b/xwords4/android/jni/andutils.h @@ -92,7 +92,8 @@ jbyteArray streamToBArray( JNIEnv *env, XWStreamCtxt* stream ); jmethodID getMethodID( JNIEnv* env, jobject obj, const char* proc, const char* sig ); -void setJAddrRec( JNIEnv* env, jobject jaddr, const CommsAddrRec* addr ); +jobject makeJAddr( JNIEnv* env, const CommsAddrRec* addr ); +jobject setJAddrRec( JNIEnv* env, jobject jaddr, const CommsAddrRec* addr ); void getJAddrRec( JNIEnv* env, CommsAddrRec* addr, jobject jaddr ); void setTypeSetFieldIn( JNIEnv* env, const CommsAddrRec* addr, jobject jTarget, const char* fldName ); @@ -104,6 +105,10 @@ void intToJenumField( JNIEnv* env, jobject jobj, int val, const char* field, jobject intToJEnum( JNIEnv* env, int val, const char* enumSig ); jint jEnumToInt( JNIEnv* env, jobject jenum ); +#define AANDS(a) (a), VSIZE(a) +void loadNLI( JNIEnv* env, NetLaunchInfo* nli, jobject jnli ); +void setNLI( JNIEnv* env, jobject jnli, const NetLaunchInfo* nli ); + XP_U32 getCurSeconds( JNIEnv* env ); void deleteLocalRef( JNIEnv* env, jobject jobj ); diff --git a/xwords4/android/jni/drawwrapper.c b/xwords4/android/jni/drawwrapper.c index 9ae0d533f..a461803a6 100644 --- a/xwords4/android/jni/drawwrapper.c +++ b/xwords4/android/jni/drawwrapper.c @@ -210,12 +210,14 @@ makeDSI( AndDraw* draw, XWEnv xwe, int indx, const DrawScoreInfo* dsi ) } #endif -#define DRAW_CBK_HEADER(nam,sig) \ +#define DRAW_CBK_HEADER(nam,sig) { \ JNIEnv* env = xwe; \ AndDraw* draw = (AndDraw*)dctx; \ ASSERT_ENV( draw->ti, env ); \ XP_ASSERT( !!draw->jdraw ); \ - jmethodID mid = getMethodID( xwe, draw->jdraw, nam, sig ); + jmethodID mid = getMethodID( xwe, draw->jdraw, nam, sig ) \ + +#define DRAW_CBK_HEADER_END() } static XP_Bool and_draw_scoreBegin( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, @@ -238,6 +240,7 @@ and_draw_scoreBegin( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, returnJRect( draw, JCACHE_RECT0, jrect ); deleteLocalRef( env, jscores ); + DRAW_CBK_HEADER_END(); return result; } @@ -255,6 +258,7 @@ and_draw_drawRemText( DrawCtx* dctx, XP_S16 nTilesLeft, readJRect( env, rect, jrect ); } returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); return result; } @@ -277,6 +281,7 @@ and_draw_score_drawPlayers( DrawCtx* dctx, XWEnv xwe, const XP_Rect* scoreRect, readJRect( env, &playerRects[ii], jrect ); } returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); } #else @@ -286,15 +291,16 @@ and_draw_measureRemText( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, XP_S16 nTilesLeft, XP_U16* width, XP_U16* height ) { + jboolean result; DRAW_CBK_HEADER("measureRemText", "(Landroid/graphics/Rect;I[I[I)Z" ); jintArray widthArray = (*env)->NewIntArray( env, 1 ); jintArray heightArray = (*env)->NewIntArray( env, 1 ); jobject jrect = makeJRect( draw, xwe, JCACHE_RECT0, rect ); - jboolean result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, jrect, - nTilesLeft, widthArray, - heightArray ); + result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, jrect, + nTilesLeft, widthArray, + heightArray ); if ( result ) { int tmp; getIntsFromArray( env, &tmp, widthArray, 1, true ); @@ -303,6 +309,7 @@ and_draw_measureRemText( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, *height = tmp; } returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); return result; } /* and_draw_measureRemText */ @@ -321,6 +328,7 @@ and_draw_drawRemText( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rInner, nTilesLeft, focussed ); returnJRect( draw, JCACHE_RECT0, jrinner ); returnJRect( draw, JCACHE_RECT1, jrouter ); + DRAW_CBK_HEADER_END(); } static void @@ -347,6 +355,7 @@ and_draw_measureScoreText( DrawCtx* dctx, XWEnv xwe, const XP_Rect* r, *width = tmp; getIntsFromArray( env, &tmp, heightArray, 1, true ); *height = tmp; + DRAW_CBK_HEADER_END(); } /* and_draw_measureScoreText */ static void @@ -366,6 +375,7 @@ and_draw_score_drawPlayer( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rInner, jdsi ); returnJRect( draw, JCACHE_RECT0, jrinner ); returnJRect( draw, JCACHE_RECT1, jrouter ); + DRAW_CBK_HEADER_END(); } /* and_draw_score_drawPlayer */ #endif @@ -382,6 +392,7 @@ and_draw_drawTimer( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, XP_U16 player (*env)->CallVoidMethod( env, draw->jdraw, mid, jrect, player, secondsLeft, inDuplicateMode ); returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); } } @@ -408,6 +419,7 @@ and_draw_drawCell( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, XP_S16 owner, XWBonusType bonus, HintAtts hintAtts, CellFlags flags ) { + jboolean result; DRAW_CBK_HEADER("drawCell", "(Landroid/graphics/Rect;Ljava/lang/String;IIIIII)Z" ); jobject jrect = makeJRect( draw, xwe, JCACHE_RECT0, rect ); @@ -419,13 +431,14 @@ and_draw_drawCell( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, jtext = (*env)->NewStringUTF( env, text ); } - jboolean result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, - jrect, jtext, tile, value, - owner, bonus, hintAtts, - flags ); + result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, + jrect, jtext, tile, value, + owner, bonus, hintAtts, + flags ); returnJRect( draw, JCACHE_RECT0, jrect ); deleteLocalRef( env, jtext ); + DRAW_CBK_HEADER_END(); return result; } @@ -440,6 +453,7 @@ and_draw_drawBoardArrow( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, (*env)->CallVoidMethod( env, draw->jdraw, mid, jrect, bonus, vert, hintAtts, flags ); returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); } static XP_Bool @@ -456,13 +470,14 @@ static XP_Bool and_draw_trayBegin( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, XP_U16 owner, XP_S16 score, DrawFocusState XP_UNUSED(dfs) ) { + jboolean result; DRAW_CBK_HEADER( "trayBegin", "(Landroid/graphics/Rect;II)Z" ); jobject jrect = makeJRect( draw, xwe, JCACHE_RECT0, rect ); - jboolean result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, - jrect, owner, score ); + result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, jrect, owner, score ); returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); return result; } @@ -484,6 +499,7 @@ and_draw_drawTile( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, jrect, jtext, val, flags ); returnJRect( draw, JCACHE_RECT0, jrect ); deleteLocalRef( env, jtext ); + DRAW_CBK_HEADER_END(); return result; } @@ -506,18 +522,21 @@ and_draw_drawTileMidDrag( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, jrect, jtext, val, owner, flags ); returnJRect( draw, JCACHE_RECT0, jrect ); deleteLocalRef( env, jtext ); + DRAW_CBK_HEADER_END(); return result; } static XP_Bool and_draw_drawTileBack( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, CellFlags flags ) { + XP_Bool result; DRAW_CBK_HEADER( "drawTileBack", "(Landroid/graphics/Rect;I)Z" ); jobject jrect = makeJRect( draw, xwe, JCACHE_RECT0, rect ); - XP_Bool result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, jrect, flags ); + result = (*env)->CallBooleanMethod( env, draw->jdraw, mid, jrect, flags ); returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); return result; } @@ -531,6 +550,7 @@ and_draw_drawTrayDivider( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, CellFla (*env)->CallVoidMethod( env, draw->jdraw, mid, jrect, flags ); returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); } static void @@ -545,6 +565,7 @@ and_draw_score_pendingScore( DrawCtx* dctx, XWEnv xwe, const XP_Rect* rect, (*env)->CallVoidMethod( env, draw->jdraw, mid, jrect, score, playerNum, curTurn, flags ); returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); } static void @@ -559,6 +580,7 @@ and_draw_objFinished( DrawCtx* dctx, XWEnv xwe, BoardObjectType typ, (*env)->CallVoidMethod( env, draw->jdraw, mid, (jint)typ, jrect ); returnJRect( draw, JCACHE_RECT0, jrect ); + DRAW_CBK_HEADER_END(); #endif } @@ -568,7 +590,7 @@ and_draw_dictChanged( DrawCtx* dctx, XWEnv xwe, XP_S16 playerNum, { AndDraw* draw = (AndDraw*)dctx; if ( !!dict && !!draw->jdraw ) { - XP_LOGF( "%s(dict=%p); code=%x", __func__, dict, andDictID(dict) ); + XP_LOGFF( "(dict=%p/%s); code=%x", dict, dict_getName(dict), andDictID(dict) ); XP_LangCode code = 0; /* A null dict means no-lang */ if ( NULL != dict ) { code = dict_getLangCode( dict ); @@ -587,6 +609,7 @@ and_draw_dictChanged( DrawCtx* dctx, XWEnv xwe, XP_S16 playerNum, /* jobject jdict = (*env)->NewObject( env, rclass, initId, (int)dict ); */ (*env)->CallVoidMethod( env, draw->jdraw, mid, (jlong)dict ); + DRAW_CBK_HEADER_END(); } } } @@ -602,6 +625,7 @@ and_draw_getMiniWText( DrawCtx* dctx, XWEnv xwe, XWMiniTextType textHint ) snprintf( draw->miniTextBuf, VSIZE(draw->miniTextBuf), "%s", str ); (*env)->ReleaseStringUTFChars( env, jstr, str ); deleteLocalRef( env, jstr ); + DRAW_CBK_HEADER_END(); return draw->miniTextBuf; } @@ -625,6 +649,7 @@ and_draw_measureMiniWText( DrawCtx* dctx, XWEnv xwe, const XP_UCHAR* textP, *width = tmp; getIntsFromArray( env, &tmp, heightArray, 1, true ); *height = tmp; + DRAW_CBK_HEADER_END(); } static void @@ -641,6 +666,7 @@ and_draw_drawMiniWindow( DrawCtx* dctx, XWEnv xwe, const XP_UCHAR* text, jstr, jrect ); returnJRect( draw, JCACHE_RECT0, jrect ); deleteLocalRef( env, jstr ); + DRAW_CBK_HEADER_END(); } #endif diff --git a/xwords4/android/jni/utilwrapper.c b/xwords4/android/jni/utilwrapper.c index 1526580da..9facc6d15 100644 --- a/xwords4/android/jni/utilwrapper.c +++ b/xwords4/android/jni/utilwrapper.c @@ -28,6 +28,7 @@ #include "paths.h" #include "LocalizedStrIncludes.h" #include "dbgutil.h" +#include "nli.h" #define MAX_QUANTITY_STRS 4 @@ -523,16 +524,17 @@ and_dutil_loadPtr( XW_DUtilCtxt* duc, XWEnv xwe, const XP_UCHAR* key, { JNIEnv* env = xwe; jbyteArray jvalue = loadToByteArray( duc, env, key ); + jsize len = 0; if ( jvalue != NULL ) { - jsize len = (*env)->GetArrayLength( env, jvalue ); + len = (*env)->GetArrayLength( env, jvalue ); if ( len <= *lenp ) { jbyte* jelems = (*env)->GetByteArrayElements( env, jvalue, NULL ); XP_MEMCPY( data, jelems, len ); (*env)->ReleaseByteArrayElements( env, jvalue, jelems, 0 ); } - *lenp = len; deleteLocalRef( env, jvalue ); } + *lenp = len; } static void @@ -847,6 +849,64 @@ and_dutil_onDupTimerChanged( XW_DUtilCtxt* duc, XWEnv xwe, XP_U32 gameID, DUTIL_CBK_TAIL(); } +static void +and_dutil_onInviteReceived( XW_DUtilCtxt* duc, XWEnv xwe, const NetLaunchInfo* nli ) +{ + LOGNLI( nli ); + DUTIL_CBK_HEADER( "onInviteReceived", "(L" PKG_PATH("NetLaunchInfo") ";)V" ); + + /* Allocate a new NetLaunchInfo */ + jclass cls = (*env)->FindClass( env, PKG_PATH("NetLaunchInfo") ); + XP_ASSERT( !!cls ); + jmethodID initId = (*env)->GetMethodID( env, cls, "", "()V" ); + XP_ASSERT( !!initId ); + jobject jnli = (*env)->NewObject( env, cls, initId ); + XP_ASSERT( !!jnli ); + setNLI( env, jnli, nli ); + + (*env)->CallVoidMethod( env, dutil->jdutil, mid, jnli ); + + deleteLocalRefs( env, jnli, cls, DELETE_NO_REF ); + DUTIL_CBK_TAIL(); +} + +static void +and_dutil_onMessageReceived( XW_DUtilCtxt* duc, XWEnv xwe, XP_U32 gameID, + const CommsAddrRec* from, XWStreamCtxt* stream ) +{ + LOG_FUNC(); + DUTIL_CBK_HEADER( "onMessageReceived", + "(IL" PKG_PATH("jni/CommsAddrRec") ";[B)V" ); + + XP_U16 len = stream_getSize( stream ); + XP_U8 data[len]; + stream_getBytes( stream, data, len ); + + jbyteArray jmsg = makeByteArray( env, len, (jbyte*)data ); + + jobject jaddr = makeJAddr( env, from ); + + (*env)->CallVoidMethod( env, dutil->jdutil, mid, gameID, jaddr, jmsg ); + + deleteLocalRefs( env, jmsg, jaddr, DELETE_NO_REF ); + + DUTIL_CBK_TAIL(); + LOG_RETURN_VOID(); +} + +static void +and_dutil_onGameGoneReceived( XW_DUtilCtxt* duc, XWEnv xwe, XP_U32 gameID, + const CommsAddrRec* from ) +{ + DUTIL_CBK_HEADER( "onGameGoneReceived", + "(IL" PKG_PATH("jni/CommsAddrRec") ";)V" ); + jobject jaddr = makeJAddr( env, from ); + (*env)->CallVoidMethod( env, dutil->jdutil, mid, gameID, jaddr ); + + deleteLocalRefs( env, jaddr, DELETE_NO_REF ); + DUTIL_CBK_TAIL(); +} + XW_UtilCtxt* makeUtil( MPFORMAL JNIEnv* env, #ifdef MAP_THREAD_TO_ENV @@ -994,6 +1054,10 @@ makeDUtil( MPFORMAL JNIEnv* env, SET_DPROC(notifyPause); SET_DPROC(onDupTimerChanged); + SET_DPROC(onInviteReceived); + SET_DPROC(onMessageReceived); + SET_DPROC(onGameGoneReceived); + #undef SET_DPROC assertTableFull( vtable, sizeof(*vtable), "dutil" ); diff --git a/xwords4/android/jni/xportwrapper.c b/xwords4/android/jni/xportwrapper.c index fe9298e0c..017a78d0d 100644 --- a/xwords4/android/jni/xportwrapper.c +++ b/xwords4/android/jni/xportwrapper.c @@ -32,27 +32,6 @@ typedef struct _AndTransportProcs { MPSLOT } AndTransportProcs; -static jobject -makeJAddr( JNIEnv* env, const CommsAddrRec* addr ) -{ - jobject jaddr = NULL; - if ( NULL != addr ) { - jclass clazz - = (*env)->FindClass(env, PKG_PATH("jni/CommsAddrRec") ); - XP_ASSERT( !!clazz ); - jmethodID mid = getMethodID( env, clazz, "", "()V" ); - XP_ASSERT( !!mid ); - - jaddr = (*env)->NewObject( env, clazz, mid ); - XP_ASSERT( !!jaddr ); - - setJAddrRec( env, jaddr, addr ); - - deleteLocalRef( env, clazz ); - } - return jaddr; -} - static XP_U32 and_xport_getFlags( XWEnv xwe, void* closure ) { diff --git a/xwords4/android/jni/xwjni.c b/xwords4/android/jni/xwjni.c index 3fc484d6d..8780d9133 100644 --- a/xwords4/android/jni/xwjni.c +++ b/xwords4/android/jni/xwjni.c @@ -35,6 +35,7 @@ #include "dictmgr.h" #include "nli.h" #include "smsproto.h" +#include "device.h" #include "utilwrapper.h" #include "drawwrapper.h" @@ -434,9 +435,6 @@ static const SetInfo pl_ints[] = { ,ARR_MEMBER( LocalPlayer, secondsUsed ) }; -#define AANDS(a) \ - (a), VSIZE(a) - static CurGameInfo* makeGI( MPFORMAL JNIEnv* env, jobject jgi ) { @@ -498,47 +496,6 @@ makeGI( MPFORMAL JNIEnv* env, jobject jgi ) return gi; } /* makeGI */ -static const SetInfo nli_ints[] = { - ARR_MEMBER( NetLaunchInfo, _conTypes ), - ARR_MEMBER( NetLaunchInfo, lang ), - ARR_MEMBER( NetLaunchInfo, forceChannel ), - ARR_MEMBER( NetLaunchInfo, nPlayersT ), - ARR_MEMBER( NetLaunchInfo, nPlayersH ), - ARR_MEMBER( NetLaunchInfo, gameID ), - ARR_MEMBER( NetLaunchInfo, osVers ), -}; - -static const SetInfo nli_bools[] = { - ARR_MEMBER( NetLaunchInfo, isGSM ), - ARR_MEMBER( NetLaunchInfo, remotesAreRobots ), -}; - -static const SetInfo nli_strs[] = { - ARR_MEMBER( NetLaunchInfo, dict ), - ARR_MEMBER( NetLaunchInfo, gameName ), - ARR_MEMBER( NetLaunchInfo, room ), - ARR_MEMBER( NetLaunchInfo, btName ), - ARR_MEMBER( NetLaunchInfo, btAddress ), - ARR_MEMBER( NetLaunchInfo, phone ), - ARR_MEMBER( NetLaunchInfo, inviteID ), -}; - -static void -loadNLI( JNIEnv* env, NetLaunchInfo* nli, jobject jnli ) -{ - getInts( env, (void*)nli, jnli, AANDS(nli_ints) ); - getBools( env, (void*)nli, jnli, AANDS(nli_bools) ); - getStrings( env, (void*)nli, jnli, AANDS(nli_strs) ); -} - -static void -setNLI( JNIEnv* env, jobject jnli, const NetLaunchInfo* nli ) -{ - setInts( env, jnli, (void*)nli, AANDS(nli_ints) ); - setBools( env, jnli, (void*)nli, AANDS(nli_bools) ); - setStrings( env, jnli, (void*)nli, AANDS(nli_strs) ); -} - static void setJGI( JNIEnv* env, jobject jgi, const CurGameInfo* gi ) { @@ -653,8 +610,153 @@ streamFromJStream( MPFORMAL JNIEnv* env, VTableMgr* vtMgr, jbyteArray jstream ) } /* streamFromJStream */ /**************************************************** - * These three methods are stateless: no gamePtr + * These methods are stateless: no gamePtr ****************************************************/ + +#define DVC_HEADER(PTR) { \ + JNIGlobalState* globalState = (JNIGlobalState*)(PTR); \ + +#define DVC_HEADER_END() } \ + + +JNIEXPORT jstring JNICALL +Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1getMQTTDevID +( JNIEnv* env, jclass C, jlong jniGlobalPtr, jobjectArray jTopicOut ) +{ + jstring result; + DVC_HEADER(jniGlobalPtr); + MQTTDevID devID; + dvc_getMQTTDevID( globalState->dutil, env, &devID ); + + XP_UCHAR buf[64]; + + if ( !!jTopicOut ) { + formatMQTTTopic( &devID, buf, VSIZE(buf) ); + jstring jtopic = (*env)->NewStringUTF( env, buf ); + XP_ASSERT( 1 == (*env)->GetArrayLength( env, jTopicOut ) ); /* fired */ + (*env)->SetObjectArrayElement( env, jTopicOut, 0, jtopic ); + deleteLocalRef( env, jtopic ); + } + + formatMQTTDevID( &devID, buf, VSIZE(buf) ); + result = (*env)->NewStringUTF( env, buf ); + DVC_HEADER_END(); + return result; +} + +static void +addrToTopic( JNIEnv* env, jobjectArray jAddrToTopic ) +{ + XP_ASSERT( 1 == (*env)->GetArrayLength( env, jAddrToTopic ) ); + jstring jaddr = (*env)->GetObjectArrayElement( env, jAddrToTopic, 0 ); + const char* addr = (*env)->GetStringUTFChars( env, jaddr, NULL ); + + MQTTDevID devID; +#ifdef DEBUG + XP_Bool success = +#endif + strToMQTTCDevID( addr, &devID ); + XP_ASSERT( success ); + + XP_UCHAR buf[64]; + formatMQTTTopic( &devID, buf, VSIZE(buf) ); + jstring jTopic = (*env)->NewStringUTF( env, buf ); + (*env)->SetObjectArrayElement( env, jAddrToTopic, 0, jTopic ); + + (*env)->ReleaseStringUTFChars( env, jaddr, addr ); + deleteLocalRefs( env, jaddr, jTopic, DELETE_NO_REF ); +} + +JNIEXPORT jbyteArray JNICALL +Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1makeMQTTInvite +( JNIEnv* env, jclass C, jlong jniGlobalPtr, jobject jnli, + jobjectArray jAddrToTopic ) +{ + jbyteArray result; + DVC_HEADER(jniGlobalPtr); + NetLaunchInfo nli = {0}; + loadNLI( env, &nli, jnli ); + LOGNLI( &nli ); + + XWStreamCtxt* stream = mem_stream_make( MPPARM(globalState->mpool) + globalState->vtMgr, + NULL, 0, NULL ); + dvc_makeMQTTInvite( stream, &nli ); + + result = streamToBArray( env, stream ); + stream_destroy( stream, env ); + + addrToTopic( env, jAddrToTopic ); + + DVC_HEADER_END(); + return result; +} + +JNIEXPORT jbyteArray JNICALL +Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1makeMQTTMessage +( JNIEnv* env, jclass C, jlong jniGlobalPtr, jint jGameID, + jbyteArray jmsg, jobjectArray jAddrToTopic ) +{ + jbyteArray result; + LOG_FUNC(); + DVC_HEADER(jniGlobalPtr); + + XWStreamCtxt* stream = mem_stream_make( MPPARM(globalState->mpool) + globalState->vtMgr, + NULL, 0, NULL ); + + XP_U16 len = (*env)->GetArrayLength( env, jmsg ); + jbyte* buf = (*env)->GetByteArrayElements( env, jmsg, NULL ); + dvc_makeMQTTMessage( globalState->dutil, env, stream, jGameID, + (const XP_U8*)buf, len ); + (*env)->ReleaseByteArrayElements( env, jmsg, buf, 0 ); + + result = streamToBArray( env, stream ); + stream_destroy( stream, env ); + + addrToTopic( env, jAddrToTopic ); + + DVC_HEADER_END(); + return result; +} + +JNIEXPORT jbyteArray JNICALL +Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1makeMQTTNoSuchGame +( JNIEnv* env, jclass C, jlong jniGlobalPtr, jint jgameid, jobjectArray jAddrToTopic ) +{ + jbyteArray result; + DVC_HEADER(jniGlobalPtr); + + XWStreamCtxt* stream = mem_stream_make( MPPARM(globalState->mpool) + globalState->vtMgr, + NULL, 0, NULL ); + dvc_makeMQTTNoSuchGame( globalState->dutil, env, stream, jgameid ); + + result = streamToBArray( env, stream ); + stream_destroy( stream, env ); + + addrToTopic( env, jAddrToTopic ); + + DVC_HEADER_END(); + LOG_RETURN_VOID(); + return result; +} + +JNIEXPORT void JNICALL +Java_org_eehouse_android_xw4_jni_XwJNI_dvc_1parseMQTTPacket +( JNIEnv* env, jclass C, jlong jniGlobalPtr, jbyteArray jmsg ) +{ + DVC_HEADER(jniGlobalPtr); + + XP_U16 len = (*env)->GetArrayLength( env, jmsg ); + jbyte* buf = (*env)->GetByteArrayElements( env, jmsg, NULL ); + + dvc_parseMQTTPacket( globalState->dutil, env, (XP_U8*)buf, len ); + + (*env)->ReleaseByteArrayElements( env, jmsg, buf, 0 ); + DVC_HEADER_END(); +} + JNIEXPORT jbyteArray JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_gi_1to_1stream ( JNIEnv* env, jclass C, jlong jniGlobalPtr, jobject jgi ) @@ -705,7 +807,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_gi_1from_1stream JNIEXPORT jbyteArray JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_nli_1to_1stream -( JNIEnv* env, jclass C, jlong jniGlobalPtr, jobject njli ) +( JNIEnv* env, jclass C, jlong jniGlobalPtr, jobject jnli ) { LOG_FUNC(); JNIGlobalState* globalState = (JNIGlobalState*)jniGlobalPtr; @@ -715,7 +817,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_nli_1to_1stream jbyteArray result; NetLaunchInfo nli = {0}; - loadNLI( env, &nli, njli ); + loadNLI( env, &nli, jnli ); /* CurGameInfo* gi = makeGI( MPPARM(mpool) env, jgi ); */ XWStreamCtxt* stream = mem_stream_make( MPPARM(mpool) globalState->vtMgr, NULL, 0, NULL ); @@ -1877,10 +1979,8 @@ Java_org_eehouse_android_xw4_jni_XwJNI_comms_1getAddrs jclass clas = (*env)->FindClass( env, PKG_PATH("jni/CommsAddrRec") ); result = (*env)->NewObjectArray( env, count, clas, NULL ); - jmethodID initId = (*env)->GetMethodID( env, clas, "", "()V" ); for ( int ii = 0; ii < count; ++ii ) { - jobject jaddr = (*env)->NewObject( env, clas, initId ); - setJAddrRec( env, jaddr, &addrs[ii] ); + jobject jaddr = makeJAddr( env, &addrs[ii] ); (*env)->SetObjectArrayElement( env, result, ii, jaddr ); deleteLocalRef( env, jaddr ); } @@ -1976,6 +2076,7 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1summarize } break; case COMMS_CONN_NFC: + case COMMS_CONN_MQTT: break; #if defined XWFEATURE_BLUETOOTH || defined XWFEATURE_SMS || defined XWFEATURE_P2P case COMMS_CONN_BT: diff --git a/xwords4/common/board.h b/xwords4/common/board.h index 71916412e..1216d3378 100644 --- a/xwords4/common/board.h +++ b/xwords4/common/board.h @@ -27,9 +27,6 @@ #include "draw.h" #include "xwstream.h" -/* typedef struct BoardVTable { */ -/* } BoardVTable; */ - #ifdef CPLUS extern "C" { #endif diff --git a/xwords4/common/comms.c b/xwords4/common/comms.c index b9783bb96..8b462198b 100644 --- a/xwords4/common/comms.c +++ b/xwords4/common/comms.c @@ -622,6 +622,9 @@ addrFromStreamOne( CommsAddrRec* addrP, XWStreamCtxt* stream, CommsConnType typ break; case COMMS_CONN_NFC: break; + case COMMS_CONN_MQTT: + stream_getBytes( stream, &addrP->u.mqtt.devID, sizeof(addrP->u.mqtt.devID) ); + break; default: /* shut up, compiler */ break; @@ -872,6 +875,9 @@ addrToStreamOne( XWStreamCtxt* stream, CommsConnType typ, const CommsAddrRec* ad break; case COMMS_CONN_NFC: break; + case COMMS_CONN_MQTT: + stream_putBytes( stream, &addrP->u.mqtt.devID, sizeof(addrP->u.mqtt.devID) ); + break; default: XP_ASSERT(0); break; @@ -2021,6 +2027,7 @@ preProcess( CommsCtxt* comms, XWEnv xwe, const CommsAddrRec* useAddr, case COMMS_CONN_P2P: break; /* nothing to grab?? */ case COMMS_CONN_NFC: + case COMMS_CONN_MQTT: break; /* nothing to grab?? */ default: XP_ASSERT(0); @@ -2109,8 +2116,8 @@ getRecordFor( CommsCtxt* comms, XWEnv xwe, const CommsAddrRec* addr, } } - XP_LOGF( "%s(%s, maskChannel=%s) => %p", __func__, - cbuf, maskChannel? "true":"false", rec ); + XP_LOGFF( "(%s, maskChannel=%s) => %p", cbuf, + maskChannel? "true":"false", rec ); return rec; } /* getRecordFor */ @@ -2503,17 +2510,24 @@ comms_isConnected( const CommsCtxt* const comms ) XP_Bool result = XP_FALSE; CommsConnType typ; for ( XP_U32 st = 0; !result && addr_iter( &comms->addr, &typ, &st ); ) { + XP_Bool expected = XP_FALSE; switch ( typ ) { case COMMS_CONN_RELAY: result = 0 != comms->rr.connName[0]; + expected = XP_TRUE; break; case COMMS_CONN_SMS: case COMMS_CONN_BT: case COMMS_CONN_P2P: - result = comms->connID != CONN_ID_NONE; + case COMMS_CONN_MQTT: + expected = XP_TRUE; default: + result = comms->connID != CONN_ID_NONE; break; } + if ( ! expected ) { + XP_LOGFF( "unexpected type %s", ConnType2Str(typ) ); + } } return result; } @@ -2666,6 +2680,7 @@ ConnType2Str( CommsConnType typ ) CASESTR( COMMS_CONN_P2P ); CASESTR( COMMS_CONN_NTYPES ); CASESTR( COMMS_CONN_NFC ); + CASESTR( COMMS_CONN_MQTT ); default: XP_ASSERT(0); } @@ -2819,6 +2834,13 @@ logAddr( const CommsCtxt* comms, XWEnv xwe, break; case COMMS_CONN_NFC: break; + case COMMS_CONN_MQTT: { + stream_catString( stream, "mqtt devID: " ); + XP_UCHAR buf[32]; + XP_SNPRINTF( buf, VSIZE(buf), MQTTDevID_FMT, addr->u.mqtt.devID ); + stream_catString( stream, buf ); + } + break; default: XP_ASSERT(0); } @@ -2899,6 +2921,11 @@ augmentAddr( CommsAddrRec* destAddr, const CommsAddrRec* srcAddr ) #endif case COMMS_CONN_NFC: break; + case COMMS_CONN_MQTT: + dest = &destAddr->u.mqtt; + src = &srcAddr->u.mqtt; + siz = sizeof(destAddr->u.mqtt); + break; default: XP_ASSERT(0); break; diff --git a/xwords4/common/comms.h b/xwords4/common/comms.h index f5fa4d11e..513de1050 100644 --- a/xwords4/common/comms.h +++ b/xwords4/common/comms.h @@ -22,6 +22,7 @@ #define _COMMS_H_ #include "comtypes.h" +#include "commstyp.h" #include "mempool.h" #include "xwrelay.h" #include "server.h" @@ -43,6 +44,7 @@ typedef enum { ,COMMS_CONN_SMS ,COMMS_CONN_P2P /* a.k.a. Wifi direct */ ,COMMS_CONN_NFC + ,COMMS_CONN_MQTT ,COMMS_CONN_NTYPES } CommsConnType; @@ -66,57 +68,12 @@ typedef enum { # define XW_BT_NAME "CrossWords" #endif -/* on Palm BtLibDeviceAddressType is a 48-bit quantity. Linux's typeis the - same size. Goal is something all platforms support */ -typedef struct XP_BtAddr { XP_U8 bits[6]; } XP_BtAddr; -typedef struct XP_BtAddrStr { XP_UCHAR chars[18]; } XP_BtAddrStr; - #ifdef COMMS_HEARTBEAT # define IF_CH(a) a, #else # define IF_CH(a) #endif -#define MAX_HOSTNAME_LEN 63 -#define MAX_PHONE_LEN 31 -#define MAX_P2P_MAC_LEN 17 - -typedef struct _CommsAddrRec { - XP_U16 _conTypes; - - struct { - struct { - XP_UCHAR hostName_ip[MAX_HOSTNAME_LEN + 1]; - XP_U32 ipAddr_ip; /* looked up from above */ - XP_U16 port_ip; - } ip; - struct { - XP_UCHAR invite[MAX_INVITE_LEN + 1]; /* room!!!! */ - XP_UCHAR hostName[MAX_HOSTNAME_LEN + 1]; - XP_U32 ipAddr; /* looked up from above */ - XP_U16 port; - XP_Bool seeksPublicRoom; - XP_Bool advertiseRoom; - } ip_relay; - struct { - /* nothing? */ - XP_UCHAR foo; /* wince doesn't like nothing here */ - } ir; - struct { - /* guests can browse for the host to connect to */ - XP_UCHAR hostName[MAX_HOSTNAME_LEN + 1]; - XP_BtAddrStr btAddr; - } bt; - struct { - XP_UCHAR phone[MAX_PHONE_LEN + 1]; - XP_U16 port; - } sms; - struct { - XP_UCHAR mac_addr[MAX_P2P_MAC_LEN + 1]; - } p2p; - } u; -} CommsAddrRec; - typedef XP_S16 (*TransportSend)( XWEnv xwe, const XP_U8* buf, XP_U16 len, const XP_UCHAR* msgNo, const CommsAddrRec* addr, diff --git a/xwords4/common/commstyp.h b/xwords4/common/commstyp.h new file mode 100644 index 000000000..9fc59e13c --- /dev/null +++ b/xwords4/common/commstyp.h @@ -0,0 +1,75 @@ +/* -*- compile-command: "cd ../linux && make MEMDEBUG=TRUE -j3"; -*- */ +/* + * Copyright 2001 - 2020 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. + */ + +#ifndef _COMMSTYP_H_ +#define _COMMSTYP_H_ + +#include "comtypes.h" +#include "xwrelay.h" + +#define MAX_HOSTNAME_LEN 63 +#define MAX_PHONE_LEN 31 +#define MAX_P2P_MAC_LEN 17 + +/* on Palm BtLibDeviceAddressType is a 48-bit quantity. Linux's typeis the + same size. Goal is something all platforms support */ +typedef struct XP_BtAddr { XP_U8 bits[6]; } XP_BtAddr; +typedef struct XP_BtAddrStr { XP_UCHAR chars[18]; } XP_BtAddrStr; + +typedef struct _CommsAddrRec { + XP_U16 _conTypes; + + struct { + struct { + XP_UCHAR hostName_ip[MAX_HOSTNAME_LEN + 1]; + XP_U32 ipAddr_ip; /* looked up from above */ + XP_U16 port_ip; + } ip; + struct { + XP_UCHAR invite[MAX_INVITE_LEN + 1]; /* room!!!! */ + XP_UCHAR hostName[MAX_HOSTNAME_LEN + 1]; + XP_U32 ipAddr; /* looked up from above */ + XP_U16 port; + XP_Bool seeksPublicRoom; + XP_Bool advertiseRoom; + } ip_relay; + struct { + /* nothing? */ + XP_UCHAR foo; /* wince doesn't like nothing here */ + } ir; + struct { + /* guests can browse for the host to connect to */ + XP_UCHAR hostName[MAX_HOSTNAME_LEN + 1]; + XP_BtAddrStr btAddr; + } bt; + struct { + XP_UCHAR phone[MAX_PHONE_LEN + 1]; + XP_U16 port; + } sms; + struct { + MQTTDevID devID; + } mqtt; + struct { + XP_UCHAR mac_addr[MAX_P2P_MAC_LEN + 1]; + } p2p; + } u; +} CommsAddrRec; + +#endif diff --git a/xwords4/common/comtypes.h b/xwords4/common/comtypes.h index e4cfe0b30..bb6170ab6 100644 --- a/xwords4/common/comtypes.h +++ b/xwords4/common/comtypes.h @@ -246,6 +246,47 @@ typedef struct _PlayerDicts { DictionaryCtxt* dicts[MAX_NUM_PLAYERS]; } PlayerDicts; +typedef uint64_t MQTTDevID; + +#if __WORDSIZE == 64 +# define MQTTDevID_FMT "%lX" +#elif __WORDSIZE == 32 +# define MQTTDevID_FMT "%llX" +#endif +# define MQTTTopic_FMT "xw4/device/" MQTTDevID_FMT + +/* Used by scoring code and engine as fast representation of moves. */ +typedef struct _MoveInfoTile { + XP_U8 varCoord; /* 5 bits ok (0-16 for 17x17 board) */ + Tile tile; /* 6 bits will do */ +} MoveInfoTile; + +typedef struct MoveInfo { + XP_U8 nTiles; /* 4 bits: 0-7 */ + XP_U8 commonCoord; /* 5 bits: 0-16 if 17x17 possible */ + XP_Bool isHorizontal; /* 1 bit */ + /* If this is to go on an undo stack, we need player num here, or the code + has to keep track of it *and* there must be exactly one entry per + player per turn. */ + MoveInfoTile tiles[MAX_TRAY_TILES]; +} MoveInfo; + +typedef struct _LastMoveInfo { + const XP_UCHAR* names[MAX_NUM_PLAYERS]; + XP_U16 nWinners; /* >1 possible in duplicate case only */ + XP_U16 score; + XP_U16 nTiles; + XP_UCHAR word[MAX_COLS * 2]; /* be safe */ + XP_U8 moveType; + XP_Bool inDuplicateMode; +} LastMoveInfo; + +typedef XP_U8 TrayTile; +typedef struct _TrayTileSet { + XP_U8 nTiles; + TrayTile tiles[MAX_TRAY_TILES]; +} TrayTileSet; + #ifdef XWFEATURE_BLUETOOTH /* temporary debugging hack */ diff --git a/xwords4/common/device.c b/xwords4/common/device.c index e6fcdce1a..d73bfc63e 100644 --- a/xwords4/common/device.c +++ b/xwords4/common/device.c @@ -21,14 +21,8 @@ #include "comtypes.h" #include "memstream.h" #include "xwstream.h" - -#ifdef XWFEATURE_DEVICE - -# define KEY_DEVSTATE PERSIST_KEY("devState") - -typedef struct _DevCtxt { - XP_U16 devCount; -} DevCtxt; +#include "strutils.h" +#include "nli.h" static XWStreamCtxt* mkStream( XW_DUtilCtxt* dutil ) @@ -38,6 +32,13 @@ mkStream( XW_DUtilCtxt* dutil ) return stream; } +#ifdef XWFEATURE_DEVICE +# define KEY_DEVSTATE PERSIST_KEY("devState") + +typedef struct _DevCtxt { + XP_U16 devCount; +} DevCtxt; + static DevCtxt* load( XW_DUtilCtxt* dutil, XWEnv xwe ) { @@ -64,7 +65,7 @@ load( XW_DUtilCtxt* dutil, XWEnv xwe ) } void -device_store( XW_DUtilCtxt* dutil, XWEnv xwe ) +dvc_store( XW_DUtilCtxt* dutil, XWEnv xwe ) { LOG_FUNC(); DevCtxt* state = load( dutil, xwe ); @@ -77,3 +78,100 @@ device_store( XW_DUtilCtxt* dutil, XWEnv xwe ) } #endif + +#define MQTT_DEVID_KEY "mqtt_devid_key" + +void +dvc_getMQTTDevID( XW_DUtilCtxt* dutil, XWEnv xwe, MQTTDevID* devID ) +{ + MQTTDevID tmp = 0; + XP_U16 len = sizeof(tmp); + dutil_loadPtr( dutil, xwe, MQTT_DEVID_KEY, &tmp, &len ); + // XP_LOGFF( "len: %d; sizeof(tmp): %d", len, sizeof(tmp) ); + if ( len != sizeof(tmp) ) { /* we have it!!! */ + tmp = XP_RANDOM(); + tmp <<= 32; + tmp |= XP_RANDOM(); + dutil_storePtr( dutil, xwe, MQTT_DEVID_KEY, &tmp, sizeof(tmp) ); +#ifdef DEBUG + XP_UCHAR buf[32]; + formatMQTTDevID( &tmp, buf, VSIZE(buf) ); + /* This log statement is required by discon_ok2.py!!! (keep in sync) */ + XP_LOGFF( "generated id: %s", buf ); +#endif + } + *devID = tmp; + // LOG_RETURNF( MQTTDevID_FMT, *devID ); +} + +typedef enum { CMD_INVITE, CMD_MSG, CMD_DEVGONE, } MQTTCmd; + +void +dvc_makeMQTTInvite( XWStreamCtxt* stream, const NetLaunchInfo* nli ) +{ + stream_putU8( stream, CMD_INVITE ); + nli_saveToStream( nli, stream ); +} + +static void +addCmdAddrAndGameID( XW_DUtilCtxt* dutil, XWEnv xwe, MQTTCmd cmd, XP_U32 gameID, + XWStreamCtxt* stream) +{ + stream_putU8( stream, cmd ); + + MQTTDevID myID; + dvc_getMQTTDevID( dutil, xwe, &myID ); + stream_putBytes( stream, &myID, sizeof(myID) ); + + stream_putU32( stream, gameID ); +} + +void +dvc_makeMQTTMessage( XW_DUtilCtxt* dutil, XWEnv xwe, XWStreamCtxt* stream, + XP_U32 gameID, const XP_U8* buf, XP_U16 len ) +{ + addCmdAddrAndGameID( dutil, xwe, CMD_MSG, gameID, stream); + stream_putBytes( stream, buf, len ); +} + +void +dvc_makeMQTTNoSuchGame( XW_DUtilCtxt* dutil, XWEnv xwe, + XWStreamCtxt* stream, XP_U32 gameID ) +{ + addCmdAddrAndGameID( dutil, xwe, CMD_DEVGONE, gameID, stream); +} + +void +dvc_parseMQTTPacket( XW_DUtilCtxt* dutil, XWEnv xwe, const XP_U8* buf, XP_U16 len ) +{ + XWStreamCtxt* stream = mkStream( dutil ); + stream_putBytes( stream, buf, len ); + + MQTTCmd cmd = stream_getU8( stream ); + switch ( cmd ) { + case CMD_INVITE: { + NetLaunchInfo nli = {0}; + if ( nli_makeFromStream( &nli, stream ) ) { + dutil_onInviteReceived( dutil, xwe, &nli ); + } + } + break; + case CMD_DEVGONE: + case CMD_MSG: { + CommsAddrRec from = {0}; + addr_addType( &from, COMMS_CONN_MQTT ); + stream_getBytes( stream, &from.u.mqtt.devID, sizeof(from.u.mqtt.devID) ); + XP_U32 gameID = stream_getU32( stream ); + if ( CMD_MSG == cmd ) { + dutil_onMessageReceived( dutil, xwe, gameID, &from, stream ); + } else if ( CMD_DEVGONE == cmd ) { + dutil_onGameGoneReceived( dutil, xwe, gameID, &from ); + } + } + break; + default: + XP_LOGFF( "unknown command %d; dropping message", cmd ); + XP_ASSERT(0); + } + stream_destroy( stream, xwe ); +} diff --git a/xwords4/common/device.h b/xwords4/common/device.h index 3fc804817..5c4d0f0d9 100644 --- a/xwords4/common/device.h +++ b/xwords4/common/device.h @@ -25,9 +25,17 @@ // void device_load( XW_DUtilCtxt dctxt ); # ifdef XWFEATURE_DEVICE -void device_store( XW_DUtilCtxt* dctxt, XWEnv xwe ); +void dvc_store( XW_DUtilCtxt* dctxt, XWEnv xwe ); # else -# define device_store(dctxt, xwe) +# define dvc_store(dctxt, xwe) # endif +void dvc_getMQTTDevID( XW_DUtilCtxt* dutil, XWEnv xwe, MQTTDevID* devID ); + +void dvc_makeMQTTInvite( XWStreamCtxt* stream, const NetLaunchInfo* nli); +void dvc_makeMQTTMessage( XW_DUtilCtxt* dutil, XWEnv xwe, XWStreamCtxt* stream, + XP_U32 gameID, const XP_U8* buf, XP_U16 len ); +void dvc_makeMQTTNoSuchGame( XW_DUtilCtxt* dutil, XWEnv xwe, + XWStreamCtxt* stream, XP_U32 gameID ); +void dvc_parseMQTTPacket( XW_DUtilCtxt* dutil, XWEnv xwe, const XP_U8* buf, XP_U16 len ); #endif diff --git a/xwords4/common/dutil.h b/xwords4/common/dutil.h index 7f69c9fc5..7f7268b36 100644 --- a/xwords4/common/dutil.h +++ b/xwords4/common/dutil.h @@ -21,10 +21,12 @@ #ifndef _DEVUTIL_H_ #define _DEVUTIL_H_ -#include "mempool.h" #include "comtypes.h" +#include "mempool.h" #include "xwrelay.h" #include "vtabmgr.h" +#include "commstyp.h" +#include "nlityp.h" typedef enum { UNPAUSED, PAUSED, @@ -68,6 +70,13 @@ typedef struct _DUtilVtable { const XP_UCHAR* name, const XP_UCHAR* msg ); void (*m_dutil_onDupTimerChanged)( XW_DUtilCtxt* duc, XWEnv xwe, XP_U32 gameID, XP_U32 oldVal, XP_U32 newVal ); + + void (*m_dutil_onInviteReceived)( XW_DUtilCtxt* duc, XWEnv xwe, + const NetLaunchInfo* nli ); + void (*m_dutil_onMessageReceived)( XW_DUtilCtxt* duc, XWEnv xwe, XP_U32 gameID, + const CommsAddrRec* from, XWStreamCtxt* stream ); + void (*m_dutil_onGameGoneReceived)( XW_DUtilCtxt* duc, XWEnv xwe, XP_U32 gameID, + const CommsAddrRec* from ); } DUtilVtable; struct XW_DUtilCtxt { @@ -119,4 +128,13 @@ struct XW_DUtilCtxt { #define dutil_onDupTimerChanged(duc, e, id, ov, nv) \ (duc)->vtable.m_dutil_onDupTimerChanged( (duc), (e), (id), (ov), (nv)) + + +#define dutil_onInviteReceived(duc, xwe, nli) \ + (duc)->vtable.m_dutil_onInviteReceived( (duc), (xwe), (nli) ) +#define dutil_onMessageReceived(duc, xwe, gameID, from, stream) \ + (duc)->vtable.m_dutil_onMessageReceived((duc),(xwe),(gameID),(from),(stream)) +#define dutil_onGameGoneReceived(duc, xwe, gameID, from) \ + (duc)->vtable.m_dutil_onGameGoneReceived((duc),(xwe),(gameID),(from)) + #endif diff --git a/xwords4/common/model.h b/xwords4/common/model.h index 2e362a2d9..dfe664f2b 100644 --- a/xwords4/common/model.h +++ b/xwords4/common/model.h @@ -56,37 +56,6 @@ extern "C" { #define MAX_UNIQUE_TILES 64 /* max tile non-blank faces */ #define MAX_NUM_BLANKS 4 -/* Used by scoring code and engine as fast representation of moves. */ -typedef struct _MoveInfoTile { - XP_U8 varCoord; /* 5 bits ok (0-16 for 17x17 board) */ - Tile tile; /* 6 bits will do */ -} MoveInfoTile; - -typedef struct MoveInfo { - XP_U8 nTiles; /* 4 bits: 0-7 */ - XP_U8 commonCoord; /* 5 bits: 0-16 if 17x17 possible */ - XP_Bool isHorizontal; /* 1 bit */ - /* If this is to go on an undo stack, we need player num here, or the code - has to keep track of it *and* there must be exactly one entry per - player per turn. */ - MoveInfoTile tiles[MAX_TRAY_TILES]; -} MoveInfo; - -typedef struct _LastMoveInfo { - const XP_UCHAR* names[MAX_NUM_PLAYERS]; - XP_U16 nWinners; /* >1 possible in duplicate case only */ - XP_U16 score; - XP_U16 nTiles; - XP_UCHAR word[MAX_COLS * 2]; /* be safe */ - XP_U8 moveType; - XP_Bool inDuplicateMode; -} LastMoveInfo; - -typedef XP_U8 TrayTile; -typedef struct _TrayTileSet { - XP_U8 nTiles; - TrayTile tiles[MAX_TRAY_TILES]; -} TrayTileSet; typedef struct BlankQueue { XP_U16 nBlanks; diff --git a/xwords4/common/nli.c b/xwords4/common/nli.c index 10ef8c41b..a2e8dcc5b 100644 --- a/xwords4/common/nli.c +++ b/xwords4/common/nli.c @@ -55,6 +55,9 @@ nli_init( NetLaunchInfo* nli, const CurGameInfo* gi, const CommsAddrRec* addr, XP_STRCAT( nli->phone, addr->u.sms.phone ); // nli->port = addr->u.sms.port; <-- I wish break; + case COMMS_CONN_MQTT: + nli_setMQTTDevID( nli, &addr->u.mqtt.devID ); + break; default: XP_ASSERT(0); break; @@ -86,6 +89,13 @@ nli_setInviteID( NetLaunchInfo* nli, const XP_UCHAR* inviteID ) XP_STRCAT( nli->inviteID, inviteID ); } +void +nli_setMQTTDevID( NetLaunchInfo* nli, const MQTTDevID* mqttDevID ) +{ + types_addType( &nli->_conTypes, COMMS_CONN_MQTT ); + formatMQTTDevID( mqttDevID, nli->mqttDevID, VSIZE(nli->mqttDevID) ); +} + void nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream ) { @@ -118,6 +128,9 @@ nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream ) stream_putU8( stream, nli->osType ); stream_putU32( stream, nli->osVers ); } + if ( types_hasType( nli->_conTypes, COMMS_CONN_MQTT ) ) { + stringToStream( stream, nli->mqttDevID ); + } if ( NLI_VERSION > 0 ) { stream_putBits( stream, 1, nli->remotesAreRobots ? 1 : 0 ); @@ -160,6 +173,9 @@ nli_makeFromStream( NetLaunchInfo* nli, XWStreamCtxt* stream ) nli->osType= stream_getU8( stream ); nli->osVers = stream_getU32( stream ); } + if ( types_hasType( nli->_conTypes, COMMS_CONN_MQTT ) ) { + stringFromStreamHere( stream, nli->mqttDevID, sizeof(nli->mqttDevID) ); + } if ( version > 0 && 0 < stream_getSize( stream ) ) { nli->remotesAreRobots = 0 != stream_getBits( stream, 1 ); @@ -198,6 +214,16 @@ nli_makeAddrRec( const NetLaunchInfo* nli, CommsAddrRec* addr ) XP_STRCAT( addr->u.sms.phone, nli->phone ); addr->u.sms.port = 1; /* BAD, but 0 is worse */ break; + case COMMS_CONN_MQTT: { +#ifdef DEBUG + XP_Bool success = +#endif + strToMQTTCDevID( nli->mqttDevID, &addr->u.mqtt.devID ); + XP_ASSERT( success ); + } + break; + case COMMS_CONN_NFC: + break; default: XP_ASSERT(0); break; @@ -212,9 +238,10 @@ logNLI( const NetLaunchInfo* nli, const char* callerFunc, const int callerLine ) XP_LOGFF( "called by %s(), line %d", callerFunc, callerLine ); XP_UCHAR buf[256]; - XP_SNPRINTF( buf, VSIZE(buf), "{nPlayersT: %d; nPlayersH: %d; " - "gameID: %d; inviteID: %s}", - nli->nPlayersT, nli->nPlayersH, nli->gameID, nli->inviteID ); + XP_SNPRINTF( buf, VSIZE(buf), "{ctyps: %x, nPlayersT: %d; nPlayersH: %d; " + "gameID: %d; inviteID: %s, mqttid: %s}", nli->_conTypes, + nli->nPlayersT, nli->nPlayersH, nli->gameID, nli->inviteID, + nli->mqttDevID ); XP_LOGF( "%s", buf ); } # endif diff --git a/xwords4/common/nli.h b/xwords4/common/nli.h index fba479c34..4171d9e9f 100644 --- a/xwords4/common/nli.h +++ b/xwords4/common/nli.h @@ -22,63 +22,30 @@ #define _INVIT_H_ // #include "comms.h" +#include "nlityp.h" #include "xwstream.h" #include "game.h" -#define MAX_GAME_NAME_LEN 64 -#define MAX_DICT_NAME_LEN 32 - -typedef enum {OSType_NONE, OSType_LINUX, OSType_ANDROID, } XP_OSType; - /* InviteInfo * * A representation of return addresses sent with an invitation so that the * recipient has all it needs to create a game and connect back. */ -typedef struct _InviteInfo { - XP_U16 _conTypes; - - XP_UCHAR gameName[MAX_GAME_NAME_LEN]; - XP_UCHAR dict[MAX_DICT_NAME_LEN]; - XP_LangCode lang; - XP_U8 forceChannel; - XP_U8 nPlayersT; - XP_U8 nPlayersH; - XP_Bool remotesAreRobots; - XP_Bool inDuplicateMode; - - /* Relay */ - XP_UCHAR room[MAX_INVITE_LEN + 1]; - XP_U32 devID; /* not used on android; remove?? */ - - /* BT */ - XP_UCHAR btName[32]; - XP_UCHAR btAddress[32]; - - // SMS - XP_UCHAR phone[32]; - XP_Bool isGSM; - XP_OSType osType; - XP_U32 osVers; - - XP_U32 gameID; - XP_UCHAR inviteID[32]; -} NetLaunchInfo; - void -nli_init( NetLaunchInfo* invit, const CurGameInfo* gi, const CommsAddrRec* addr, +nli_init( NetLaunchInfo* nli, const CurGameInfo* gi, const CommsAddrRec* addr, XP_U16 nPlayers, XP_U16 forceChannel ); -XP_Bool nli_makeFromStream( NetLaunchInfo* invit, XWStreamCtxt* stream ); -void nli_saveToStream( const NetLaunchInfo* invit, XWStreamCtxt* stream ); +XP_Bool nli_makeFromStream( NetLaunchInfo* nli, XWStreamCtxt* stream ); +void nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream ); -void nli_makeAddrRec( const NetLaunchInfo* invit, CommsAddrRec* addr ); +void nli_makeAddrRec( const NetLaunchInfo* nli, CommsAddrRec* addr ); -void nli_setDevID( NetLaunchInfo* invit, XP_U32 devID ); -void nli_setInviteID( NetLaunchInfo* invit, const XP_UCHAR* inviteID ); -void nli_setGameName( NetLaunchInfo* invit, const XP_UCHAR* gameName ); +void nli_setDevID( NetLaunchInfo* nli, XP_U32 devID ); +void nli_setInviteID( NetLaunchInfo* nli, const XP_UCHAR* inviteID ); +void nli_setGameName( NetLaunchInfo* nli, const XP_UCHAR* gameName ); +void nli_setMQTTDevID( NetLaunchInfo* nli, const MQTTDevID* mqttDevID ); # ifdef DEBUG void logNLI( const NetLaunchInfo* nli, const char* callerFunc, const int callerLine ); diff --git a/xwords4/common/nlityp.h b/xwords4/common/nlityp.h new file mode 100644 index 000000000..fd6efc9c5 --- /dev/null +++ b/xwords4/common/nlityp.h @@ -0,0 +1,65 @@ +/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */ +/* + * Copyright 2018 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. + */ + +#ifndef _NLITYP_H_ +#define _NLITYP_H_ + +#include "comtypes.h" +#include "xwrelay.h" + +#define MAX_GAME_NAME_LEN 64 +#define MAX_DICT_NAME_LEN 32 + +typedef enum {OSType_NONE, OSType_LINUX, OSType_ANDROID, } XP_OSType; + +typedef struct _NetLaunchInfo { + XP_U16 _conTypes; + + XP_UCHAR gameName[MAX_GAME_NAME_LEN]; + XP_UCHAR dict[MAX_DICT_NAME_LEN]; + XP_LangCode lang; + XP_U8 forceChannel; + XP_U8 nPlayersT; + XP_U8 nPlayersH; + XP_Bool remotesAreRobots; + XP_Bool inDuplicateMode; + + /* Relay */ + XP_UCHAR room[MAX_INVITE_LEN + 1]; + XP_U32 devID; /* not used on android; remove?? */ + + /* BT */ + XP_UCHAR btName[32]; + XP_UCHAR btAddress[32]; + + // SMS + XP_UCHAR phone[32]; + XP_Bool isGSM; + XP_OSType osType; + XP_U32 osVers; + + XP_U32 gameID; + XP_UCHAR inviteID[32]; + /* MQTT */ + + XP_UCHAR mqttDevID[17]; +} NetLaunchInfo; + +#endif diff --git a/xwords4/common/strutils.c b/xwords4/common/strutils.c index 82054d8fc..91566715c 100644 --- a/xwords4/common/strutils.c +++ b/xwords4/common/strutils.c @@ -555,6 +555,33 @@ smsToBin( XP_U8* out, XP_U16* outlenp, const XP_UCHAR* sms, XP_U16 smslen ) #endif +const XP_UCHAR* +formatMQTTDevID( const MQTTDevID* devid, XP_UCHAR* buf, XP_U16 bufLen ) +{ + XP_SNPRINTF( buf, bufLen, MQTTDevID_FMT, *devid ); + return buf; +} + +const XP_UCHAR* +formatMQTTTopic( const MQTTDevID* devid, XP_UCHAR* buf, XP_U16 bufLen ) +{ + XP_SNPRINTF( buf, bufLen, MQTTTopic_FMT, *devid ); + // LOG_RETURNF( "%s", buf ); + return buf; +} + +XP_Bool +strToMQTTCDevID( const XP_UCHAR* str, MQTTDevID* result ) +{ + MQTTDevID tmp; + int nMatched = sscanf( str, MQTTDevID_FMT, &tmp ); + XP_Bool success = nMatched == 1; + if ( success ) { + *result = tmp; + } + return success; +} + #ifdef DEBUG #define NUM_PER_LINE 8 void diff --git a/xwords4/common/strutils.h b/xwords4/common/strutils.h index 5d80939f0..d017cc1f1 100644 --- a/xwords4/common/strutils.h +++ b/xwords4/common/strutils.h @@ -108,6 +108,10 @@ void binToSms( XP_UCHAR* out, XP_U16* outlen, const XP_U8* in, XP_U16 inlen ); XP_Bool smsToBin( XP_U8* out, XP_U16* outlen, const XP_UCHAR* in, XP_U16 inlen ); #endif +const XP_UCHAR* formatMQTTTopic( const MQTTDevID* devid, XP_UCHAR* buf, XP_U16 bufLen ); +const XP_UCHAR* formatMQTTDevID( const MQTTDevID* devid, XP_UCHAR* buf, XP_U16 bufLen ); +XP_Bool strToMQTTCDevID( const XP_UCHAR* str, MQTTDevID* result ); + #ifdef DEBUG void assertSorted( const MoveInfo* mi ); void log_hex( const XP_U8* memp, XP_U16 len, const char* tag ); diff --git a/xwords4/linux/Makefile b/xwords4/linux/Makefile index cf09929b9..92dd4d3b3 100644 --- a/xwords4/linux/Makefile +++ b/xwords4/linux/Makefile @@ -234,12 +234,14 @@ OBJ = \ $(BUILD_PLAT_DIR)/linuxutl.o \ $(BUILD_PLAT_DIR)/gamesdb.o \ $(BUILD_PLAT_DIR)/relaycon.o \ + $(BUILD_PLAT_DIR)/mqttcon.o \ $(BUILD_PLAT_DIR)/lindutil.o \ $(CURSES_OBJS) $(GTK_OBJS) $(MAIN_OBJS) LIBS = -lm -lpthread -luuid -lcurl -ljson-c $(GPROFFLAG) ifdef USE_SQLITE LIBS += -lsqlite3 +LIBS += -lmosquitto DEFINES += -DUSE_SQLITE endif # Turn this off for now. I apparently have a memory problem, but it diff --git a/xwords4/linux/curgamlistwin.c b/xwords4/linux/curgamlistwin.c index 39abb5248..a3cbfbea4 100644 --- a/xwords4/linux/curgamlistwin.c +++ b/xwords4/linux/curgamlistwin.c @@ -24,6 +24,8 @@ #include "curgamlistwin.h" #include "linuxmain.h" +#include "device.h" +#include "strutils.h" struct CursGameList { WINDOW* window; @@ -295,8 +297,12 @@ cgl_draw( CursGameList* cgl ) XP_U32 relayID = linux_getDevIDRelay( cgl->params ); char buf[cgl->width + 1]; - snprintf( buf, VSIZE(buf), "pid: %d; nGames: %d, relayid: %d", - cgl->pid, nGames, relayID ); + + MQTTDevID devID; + dvc_getMQTTDevID( cgl->params->dutil, NULL_XWE, &devID ); + XP_UCHAR didBuf[32]; + snprintf( buf, VSIZE(buf), "pid: %d; nGames: %d, relayid: %d, mqttid: %s", + cgl->pid, nGames, relayID, formatMQTTDevID( &devID, didBuf, VSIZE(didBuf) ) ); mvwaddstr( win, 0, 0, buf ); wrefresh( win ); diff --git a/xwords4/linux/cursesboard.c b/xwords4/linux/cursesboard.c index 4d7755dff..c73f74944 100644 --- a/xwords4/linux/cursesboard.c +++ b/xwords4/linux/cursesboard.c @@ -25,6 +25,7 @@ #include "linuxmain.h" #include "linuxutl.h" #include "relaycon.h" +#include "mqttcon.h" #include "cursesask.h" #include "cursesmenu.h" #include "cursesletterask.h" @@ -161,7 +162,8 @@ inviteIdle( gpointer data ) CursesBoardGlobals* bGlobals = (CursesBoardGlobals*)data; LaunchParams* params = bGlobals->cGlobals.params; if ( !!params->connInfo.relay.inviteeRelayIDs - || !!params->connInfo.sms.inviteePhones ) { + || !!params->connInfo.sms.inviteePhones + || !!params->connInfo.mqtt.inviteeDevIDs ) { handleInvite( bGlobals, 0 ); } return FALSE; @@ -1223,7 +1225,7 @@ handleReplace( void* closure, int XP_UNUSED(key) ) static bool inviteList( CommonGlobals* cGlobals, CommsAddrRec* addr, GSList* invitees, - bool useRelay ) + CommsConnType typ ) { bool haveAddressees = !!invitees; if ( haveAddressees ) { @@ -1235,14 +1237,31 @@ inviteList( CommonGlobals* cGlobals, CommsAddrRec* addr, GSList* invitees, nPlayersH, forceChannel, ii ); NetLaunchInfo nli = {0}; nli_init( &nli, cGlobals->gi, addr, nPlayersH, forceChannel ); - if ( useRelay ) { + switch ( typ ) { + case COMMS_CONN_RELAY: { uint64_t inviteeRelayID = (uint64_t)g_slist_nth_data( invitees, ii ); relaycon_invite( params, (XP_U32)inviteeRelayID, NULL, &nli ); - } else { + } + break; + case COMMS_CONN_SMS: { const gchar* inviteePhone = (const gchar*)g_slist_nth_data( invitees, ii ); linux_sms_invite( params, &nli, inviteePhone, params->connInfo.sms.port ); } + break; + case COMMS_CONN_MQTT: { + MQTTDevID devID; + const gchar* str = g_slist_nth_data( invitees, ii ); + if ( strToMQTTCDevID( str, &devID ) ) { + mqttc_invite( params, &nli, &devID ); + } else { + XP_LOGFF( "unable to convert devid %s", str ); + } + } + break; + default: + XP_ASSERT(0); + } } } return haveAddressees; @@ -1270,20 +1289,24 @@ handleInvite( void* closure, int XP_UNUSED(key) ) /* Invite first based on an invitee provided. Otherwise, fall back to doing a send-to-self. Let the recipient code reject a duplicate if the user so desires. */ - } else if ( inviteList( cGlobals, &addr, params->connInfo.sms.inviteePhones, false ) ) { + } else if ( inviteList( cGlobals, &addr, params->connInfo.sms.inviteePhones, COMMS_CONN_SMS ) ) { /* do nothing */ - } else if ( inviteList( cGlobals, &addr, params->connInfo.relay.inviteeRelayIDs, true ) ) { + } else if ( inviteList( cGlobals, &addr, params->connInfo.relay.inviteeRelayIDs, COMMS_CONN_RELAY ) ) { + /* do nothing */ + } else if ( inviteList( cGlobals, &addr, params->connInfo.mqtt.inviteeDevIDs, COMMS_CONN_MQTT ) ) { /* do nothing */ /* Try sending to self, using the phone number or relayID of this device */ } else if ( addr_hasType( &addr, COMMS_CONN_SMS ) ) { linux_sms_invite( params, &nli, addr.u.sms.phone, addr.u.sms.port ); + } else if ( addr_hasType( &addr, COMMS_CONN_MQTT ) ) { + mqttc_invite( params, &nli, mqttc_getDevID( params ) ); } else if ( addr_hasType( &addr, COMMS_CONN_RELAY ) ) { XP_U32 relayID = linux_getDevIDRelay( params ); if ( 0 != relayID ) { relaycon_invite( params, relayID, NULL, &nli ); } } else { - ca_inform( bGlobals->boardWin, "Cannot invite via relayID or by \"sms phone\"." ); + ca_inform( bGlobals->boardWin, "Cannot invite via relayID, MQTT or by \"sms phone\"." ); } LOG_RETURNF( "%s", "TRUE" ); return XP_TRUE; diff --git a/xwords4/linux/cursesmain.c b/xwords4/linux/cursesmain.c index 9a0e13577..d4d4eb284 100644 --- a/xwords4/linux/cursesmain.c +++ b/xwords4/linux/cursesmain.c @@ -64,6 +64,7 @@ #include "linuxudp.h" #include "gamesdb.h" #include "relaycon.h" +#include "mqttcon.h" #include "smsproto.h" #include "device.h" #include "cursesmenu.h" @@ -1152,10 +1153,11 @@ onJoined( void* closure, const XP_UCHAR* connname, XWHostID hid ) /* } */ #endif -static void -inviteReceivedCurses( CursesAppGlobals* aGlobals, const NetLaunchInfo* invite, +void +inviteReceivedCurses( void* closure, const NetLaunchInfo* invite, const CommsAddrRec* returnAddr ) { + CursesAppGlobals* aGlobals = (CursesAppGlobals*)closure; sqlite3_int64 rowids[1]; int nRowIDs = VSIZE(rowids); getRowsForGameID( aGlobals->cag.params->pDb, invite->gameID, rowids, &nRowIDs ); @@ -1176,7 +1178,7 @@ inviteReceivedCurses( CursesAppGlobals* aGlobals, const NetLaunchInfo* invite, } static void -relayInviteReceivedCurses( void* closure, NetLaunchInfo* invite ) +relayInviteReceivedCurses( void* closure, const NetLaunchInfo* invite ) { CursesAppGlobals* aGlobals = (CursesAppGlobals*)closure; CommsAddrRec addr = {0}; @@ -1228,6 +1230,21 @@ smsMsgReceivedCurses( void* closure, const CommsAddrRec* from, XP_U32 gameID, cb_feedGame( aGlobals->cbState, gameID, buf, len, from ); } +void +mqttMsgReceivedCurses( void* closure, const CommsAddrRec* from, + XP_U32 gameID, const XP_U8* buf, XP_U16 len ) +{ + CursesAppGlobals* aGlobals = (CursesAppGlobals*)closure; + cb_feedGame( aGlobals->cbState, gameID, buf, len, from ); +} + +void +gameGoneCurses( void* XP_UNUSED(closure), const CommsAddrRec* XP_UNUSED(from), + XP_U32 XP_UNUSED_DBG(gameID) ) +{ + XP_LOGFF( "(gameID=%d)", gameID ); +} + static void cursesGotForRow( void* XP_UNUSED(closure), const CommsAddrRec* XP_UNUSED(from), sqlite3_int64 XP_UNUSED(rowid), const XP_U8* XP_UNUSED(buf), @@ -1401,6 +1418,7 @@ cursesmain( XP_Bool XP_UNUSED(isServer), LaunchParams* params ) { memset( &g_globals, 0, sizeof(g_globals) ); g_globals.cag.params = params; + params->appGlobals = &g_globals; initCurses( &g_globals ); if ( !params->closeStdin ) { @@ -1494,6 +1512,8 @@ cursesmain( XP_Bool XP_UNUSED(isServer), LaunchParams* params ) linux_doInitialReg( params, idIsNew ); } + mqttc_init( params ); + #ifdef XWFEATURE_SMS gchar* myPhone = NULL; XP_U16 myPort = 0; @@ -1535,12 +1555,13 @@ cursesmain( XP_Bool XP_UNUSED(isServer), LaunchParams* params ) endwin(); - device_store( params->dutil, NULL_XWE ); + dvc_store( params->dutil, NULL_XWE ); if ( params->useUdp ) { relaycon_cleanup( params ); } linux_sms_cleanup( params ); + mqttc_cleanup( params ); } /* cursesmain */ #endif /* PLATFORM_NCURSES */ diff --git a/xwords4/linux/cursesmain.h b/xwords4/linux/cursesmain.h index 940fb3d86..f54e09eb0 100644 --- a/xwords4/linux/cursesmain.h +++ b/xwords4/linux/cursesmain.h @@ -75,5 +75,10 @@ void cursesDrawCtxtFree( DrawCtx* dctx ); void cursesmain( XP_Bool isServer, LaunchParams* params ); bool handleQuit( void* closure, int unused_key ); +void inviteReceivedCurses( void* aGlobals, const NetLaunchInfo* invite, + const CommsAddrRec* returnAddr ); +void mqttMsgReceivedCurses( void* closure, const CommsAddrRec* from, + XP_U32 gameID, const XP_U8* buf, XP_U16 len ); +void gameGoneCurses( void* closure, const CommsAddrRec* from, XP_U32 gameID ); #endif diff --git a/xwords4/linux/gamesdb.c b/xwords4/linux/gamesdb.c index 6d2016516..5acdcb33b 100644 --- a/xwords4/linux/gamesdb.c +++ b/xwords4/linux/gamesdb.c @@ -372,6 +372,12 @@ summarize( CommonGlobals* cGlobals ) case COMMS_CONN_IP_DIRECT: strcat( connvia, "IP" ); break; + case COMMS_CONN_MQTT: + strcat( connvia, "MQTT" ); + break; + case COMMS_CONN_NFC: /* this should be filtered out!!! */ + strcat( connvia, "NFC" ); + break; default: XP_ASSERT(0); break; @@ -703,7 +709,7 @@ FetchResult db_fetch( sqlite3* pDb, const gchar* key, gchar* buf, gint* buflen ) { XP_ASSERT( !!pDb ); - FetchResult result = NOT_THERE; + FetchResult fetchRes = NOT_THERE; char query[256]; #ifdef DEBUG int len = @@ -715,20 +721,20 @@ db_fetch( sqlite3* pDb, const gchar* key, gchar* buf, gint* buflen ) int sqlResult = sqlite3_prepare_v2( pDb, query, -1, &ppStmt, NULL ); XP_Bool found = SQLITE_OK == sqlResult; if ( found ) { - result = sqlite3_step( ppStmt ); - found = SQLITE_ROW == result; + sqlResult = sqlite3_step( ppStmt ); + found = SQLITE_ROW == sqlResult; if ( found ) { if ( getColumnText( ppStmt, 0, buf, buflen ) ) { - result = SUCCESS; + fetchRes = SUCCESS; } else { - result = BUFFER_TOO_SMALL; + fetchRes = BUFFER_TOO_SMALL; } } else if ( !!buf ) { buf[0] = '\0'; } } sqlite3_finalize( ppStmt ); - return result; + return fetchRes; } XP_Bool diff --git a/xwords4/linux/gtkboard.c b/xwords4/linux/gtkboard.c index c245333d5..fdeb9e5eb 100644 --- a/xwords4/linux/gtkboard.c +++ b/xwords4/linux/gtkboard.c @@ -72,6 +72,7 @@ #include "filestream.h" #include "gamesdb.h" #include "relaycon.h" +#include "mqttcon.h" /* static guint gtkSetupClientSocket( GtkGameGlobals* globals, int sock ); */ static void setCtrlsForTray( GtkGameGlobals* globals ); @@ -86,6 +87,7 @@ static void gtkShowFinalScores( const GtkGameGlobals* globals, XP_Bool ignoreTimeout ); static void send_invites( CommonGlobals* cGlobals, XP_U16 nPlayers, XP_U32 relayDevID, const XP_UCHAR* relayID, + MQTTDevID* mqttInviteeID, const CommsAddrRec* addrs ); #define GTK_TRAY_HT_ROWS 3 @@ -677,7 +679,7 @@ on_board_window_shown( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) CommsAddrRec addr = {0}; addrFromStream( &addr, stream ); - send_invites( cGlobals, 1, 0, relayID, NULL ); + send_invites( cGlobals, 1, 0, relayID, &addr.u.mqtt.devID, &addr ); } } stream_destroy( stream, NULL_XWE ); @@ -1403,18 +1405,20 @@ handle_invite_button( GtkWidget* XP_UNUSED(widget), GtkGameGlobals* globals ) CommsAddrRec inviteAddr = {0}; gint nPlayers = nMissing; XP_U32 relayDevID = 0; + MQTTDevID mqttInviteeID; XP_Bool confirmed = gtkInviteDlg( globals, &inviteAddr, &nPlayers, - &relayDevID ); + &relayDevID, &mqttInviteeID ); XP_LOGF( "%s: inviteDlg => %d", __func__, confirmed ); if ( confirmed ) { - send_invites( cGlobals, nPlayers, relayDevID, NULL, &inviteAddr ); + send_invites( cGlobals, nPlayers, relayDevID, NULL, &mqttInviteeID, &inviteAddr ); } } /* handle_invite_button */ static void send_invites( CommonGlobals* cGlobals, XP_U16 nPlayers, XP_U32 relayDevID, const XP_UCHAR* relayID, + MQTTDevID* mqttInviteeID, const CommsAddrRec* addrs ) { CommsAddrRec addr = {0}; @@ -1433,6 +1437,11 @@ send_invites( CommonGlobals* cGlobals, XP_U16 nPlayers, } // nli_setDevID( &nli, linux_getDevIDRelay( cGlobals->params ) ); + if ( addr_hasType( &addr, COMMS_CONN_MQTT ) ) { + const MQTTDevID* devid = mqttc_getDevID( cGlobals->params ); + nli_setMQTTDevID( &nli, devid ); + } + #ifdef DEBUG { XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(cGlobals->util->mpool) @@ -1458,6 +1467,10 @@ send_invites( CommonGlobals* cGlobals, XP_U16 nPlayers, relaycon_invite( cGlobals->params, relayDevID, relayID, &nli ); } + if ( addr_hasType( addrs, COMMS_CONN_MQTT ) ) { + mqttc_invite( cGlobals->params, &nli, mqttInviteeID ); + } + /* while ( gtkaskm( "Invite how many and how?", infos, VSIZE(infos) ) ) { */ /* int nPlayers = atoi( countStr ); */ /* if ( 0 >= nPlayers || nPlayers > nMissing ) { */ @@ -1955,7 +1968,7 @@ gtk_util_makeStreamFromAddr(XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), XP_PlayerAddr static void gtk_util_showChat( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), const XP_UCHAR* const msg, XP_S16 from, - XP_U32 timestamp ) + XP_U32 tsSecs ) { GtkGameGlobals* globals = (GtkGameGlobals*)uc->closure; XP_UCHAR buf[1024]; @@ -1963,7 +1976,13 @@ gtk_util_showChat( XW_UtilCtxt* uc, XWEnv XP_UNUSED(xwe), if ( 0 <= from ) { name = globals->cGlobals.gi->players[from].name; } - XP_SNPRINTF( buf, VSIZE(buf), "quoth %s at %d: %s", name, timestamp, msg ); + + GDateTime* dt = g_date_time_new_from_unix_utc( tsSecs ); + gchar* tsStr = g_date_time_format( dt, "%T" ); + XP_SNPRINTF( buf, VSIZE(buf), "Quoth %s at %s: \"%s\"", name, tsStr, msg ); + g_free( tsStr ); + g_date_time_unref (dt); + (void)gtkask( globals->window, buf, GTK_BUTTONS_OK, NULL ); } #endif diff --git a/xwords4/linux/gtkconnsdlg.c b/xwords4/linux/gtkconnsdlg.c index cd0bd74e6..5d5b070d6 100644 --- a/xwords4/linux/gtkconnsdlg.c +++ b/xwords4/linux/gtkconnsdlg.c @@ -125,6 +125,8 @@ handle_ok( GtkWidget* XP_UNUSED(widget), gpointer closure ) txt = gtk_entry_get_text( GTK_ENTRY(state->smsport) ); state->addr->u.sms.port = atoi( txt ); break; + case COMMS_CONN_MQTT: + break; default: XP_ASSERT( 0 ); /* keep compiler happy */ break; @@ -321,7 +323,14 @@ makeSMSPage( GtkConnsState* state, PageData* data ) gtk_widget_show( vbox ); return vbox; -} /* makeBTPage */ +} /* makeSMSPage */ + +static GtkWidget* +makeMQTTPage( GtkConnsState* state, PageData* data ) +{ + GtkWidget* vbox = boxWithUseCheck( state, data ); + return vbox; +} /* makeMQTTPage */ static PageData* getNextData( GtkConnsState* state, CommsConnType typ, gchar* label ) @@ -352,6 +361,10 @@ gtkConnsDlg( GtkGameGlobals* globals, CommsAddrRec* addr, DeviceRole role, state.notebook = gtk_notebook_new(); PageData* data; + data = getNextData( &state, COMMS_CONN_MQTT, "MQTT" ); + (void)gtk_notebook_append_page( GTK_NOTEBOOK(state.notebook), + makeMQTTPage( &state, data ), + data->label ); #ifdef XWFEATURE_RELAY data = getNextData( &state, COMMS_CONN_RELAY, "Relay" ); (void)gtk_notebook_append_page( GTK_NOTEBOOK(state.notebook), diff --git a/xwords4/linux/gtkinvit.c b/xwords4/linux/gtkinvit.c index c85a85f48..c5f910e9f 100644 --- a/xwords4/linux/gtkinvit.c +++ b/xwords4/linux/gtkinvit.c @@ -23,6 +23,8 @@ #include "gtkutils.h" #include "linuxbt.h" #include "comtypes.h" +#include "mqttcon.h" +#include "strutils.h" typedef struct _PageData { CommsConnType pageType; @@ -39,6 +41,7 @@ typedef struct _GtkInviteState { CommsAddrRec* addr; gint* nPlayersP; XP_U32* relayDevIDp; + MQTTDevID* mqttDevIDp; gint maxPlayers; GtkWidget* nPlayersCombo; @@ -50,6 +53,8 @@ typedef struct _GtkInviteState { GtkWidget* smsphone; GtkWidget* smsport; + GtkWidget* mqttDevID; + GtkWidget* bgScanButton; GtkWidget* okButton; @@ -107,12 +112,17 @@ handle_ok( GtkWidget* XP_UNUSED(widget), gpointer closure ) break; #endif case COMMS_CONN_SMS: - txt = gtk_entry_get_text( GTK_ENTRY(state->smsphone) ); - XP_STRNCPY( state->addr->u.sms.phone, txt, - sizeof(state->addr->u.sms.phone) ); txt = gtk_entry_get_text( GTK_ENTRY(state->smsport) ); state->addr->u.sms.port = atoi( txt ); break; + case COMMS_CONN_MQTT: + txt = gtk_entry_get_text( GTK_ENTRY(state->mqttDevID) ); + if ( strToMQTTCDevID( txt, &state->addr->u.mqtt.devID ) ) { + *state->mqttDevIDp = state->addr->u.mqtt.devID; + } else { + XP_ASSERT(0); + } + break; default: XP_ASSERT( 0 ); /* keep compiler happy */ break; @@ -253,6 +263,23 @@ makeSMSPage( GtkInviteState* state, PageData* data ) return vbox; } /* makeBTPage */ +static GtkWidget* +makeMQTTPage( GtkInviteState* state, PageData* data ) +{ + data->okButtonTxt = "Invite via MQTT"; + + GtkWidget* vbox = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 ); + GtkWidget* hbox; + + hbox = makeLabeledField( "Invitee MQTT DevID", &state->mqttDevID, NULL ); + // gtk_entry_set_text( GTK_ENTRY(state->mqttDevID), s_mqttIDBuf ); + gtk_box_pack_start( GTK_BOX(vbox), hbox, FALSE, TRUE, 0 ); + + gtk_widget_show( vbox ); + + return vbox; +} + static PageData* getNextData( GtkInviteState* state, CommsConnType typ, gchar* label ) { @@ -275,7 +302,8 @@ onPageChanged( GtkNotebook* XP_UNUSED(notebook), gpointer XP_UNUSED(arg1), XP_Bool gtkInviteDlg( GtkGameGlobals* globals, CommsAddrRec* addr, - gint* nPlayersP, XP_U32* relayDevIDp ) + gint* nPlayersP, XP_U32* relayDevIDp, + MQTTDevID* mqttDevIDp ) { GtkInviteState state = { .globals = globals, @@ -283,6 +311,7 @@ gtkInviteDlg( GtkGameGlobals* globals, CommsAddrRec* addr, .nPlayersP = nPlayersP, .relayDevIDp = relayDevIDp, .maxPlayers = *nPlayersP, + .mqttDevIDp = mqttDevIDp, }; GtkWidget* dialog; @@ -310,6 +339,10 @@ gtkInviteDlg( GtkGameGlobals* globals, CommsAddrRec* addr, PageData* data; + data = getNextData( &state, COMMS_CONN_MQTT, "MQTT" ); + (void)gtk_notebook_append_page( GTK_NOTEBOOK(state.notebook), + makeMQTTPage( &state, data ), + data->label ); #ifdef XWFEATURE_RELAY data = getNextData( &state, COMMS_CONN_RELAY, "Relay" ); (void)gtk_notebook_append_page( GTK_NOTEBOOK(state.notebook), diff --git a/xwords4/linux/gtkinvit.h b/xwords4/linux/gtkinvit.h index 3f5cb960d..2befaa644 100644 --- a/xwords4/linux/gtkinvit.h +++ b/xwords4/linux/gtkinvit.h @@ -24,7 +24,8 @@ /* return true if not cancelled */ XP_Bool gtkInviteDlg( GtkGameGlobals* globals, CommsAddrRec* addr, - /*inout*/ gint* nPlayers, /* out */ XP_U32* relayDevID ); + /*inout*/ gint* nPlayers, /* out */ XP_U32* relayDevID, + MQTTDevID* mqttInviteeID ); #endif diff --git a/xwords4/linux/gtkmain.c b/xwords4/linux/gtkmain.c index aadbf5845..8c30ad1c5 100644 --- a/xwords4/linux/gtkmain.c +++ b/xwords4/linux/gtkmain.c @@ -29,6 +29,7 @@ #include "linuxmain.h" #include "linuxutl.h" #include "relaycon.h" +#include "mqttcon.h" #include "linuxsms.h" #include "gtkask.h" #include "device.h" @@ -502,6 +503,13 @@ setWindowTitle( GtkAppGlobals* apg ) len = strlen( title ); snprintf( &title[len], VSIZE(title) - len, " (relayid: %d)", relayID ); #endif + len = strlen( title ); + MQTTDevID devID; + dvc_getMQTTDevID( params->dutil, NULL_XWE, &devID ); + XP_UCHAR didBuf[32]; + snprintf( &title[len], VSIZE(title) - len, " (mqtt: %s)", + formatMQTTDevID( &devID, didBuf, VSIZE(didBuf) ) ); + gtk_window_set_title( GTK_WINDOW(window), title ); } @@ -536,10 +544,19 @@ handle_relayid_to_clip( GtkWidget* XP_UNUSED(widget), GtkAppGlobals* apg ) XP_U32 relayID = linux_getDevIDRelay( params ); gchar str[32]; snprintf( &str[0], VSIZE(str), "%d", relayID ); - GtkClipboard *clipboard = gtk_clipboard_get( GDK_SELECTION_CLIPBOARD ); + GtkClipboard* clipboard = gtk_clipboard_get( GDK_SELECTION_CLIPBOARD ); gtk_clipboard_set_text( clipboard, str, strlen(str) ); } +static void +handle_mqttid_to_clip( GtkWidget* XP_UNUSED(widget), GtkAppGlobals* apg ) +{ + LaunchParams* params = apg->cag.params; + const gchar* devIDStr = mqttc_getDevIDStr( params ); + GtkClipboard* clipboard = gtk_clipboard_get( GDK_SELECTION_CLIPBOARD ); + gtk_clipboard_set_text( clipboard, devIDStr, strlen(devIDStr) ); +} + static void makeGamesWindow( GtkAppGlobals* apg ) { @@ -573,6 +590,8 @@ makeGamesWindow( GtkAppGlobals* apg ) } (void)createAddItem( netMenu, "copy relayid", (GCallback)handle_relayid_to_clip, apg ); + (void)createAddItem( netMenu, "copy mqtt devid", + (GCallback)handle_mqttid_to_clip, apg ); gtk_widget_show( menubar ); gtk_box_pack_start( GTK_BOX(vbox), menubar, FALSE, TRUE, 0 ); @@ -707,8 +726,8 @@ gameFromInvite( GtkAppGlobals* apg, const NetLaunchInfo* invite, gi_disposePlayerInfo( MPPARM(params->mpool) &gi ); } -static void -relayInviteReceived( void* closure, NetLaunchInfo* invite ) +void +relayInviteReceivedGTK( void* closure, const NetLaunchInfo* invite ) { GtkAppGlobals* apg = (GtkAppGlobals*)closure; @@ -795,9 +814,9 @@ smsInviteReceived( void* closure, const NetLaunchInfo* nli, gameFromInvite( apg, nli, returnAddr ); } -static void -smsMsgReceivedGTK( void* closure, const CommsAddrRec* from, XP_U32 gameID, - const XP_U8* buf, XP_U16 len ) +void +msgReceivedGTK( void* closure, const CommsAddrRec* from, XP_U32 gameID, + const XP_U8* buf, XP_U16 len ) { LOG_FUNC(); GtkAppGlobals* apg = (GtkAppGlobals*)closure; @@ -807,11 +826,24 @@ smsMsgReceivedGTK( void* closure, const CommsAddrRec* from, XP_U32 gameID, int nRowIDs = VSIZE(rowids); getRowsForGameID( params->pDb, gameID, rowids, &nRowIDs ); XP_LOGF( "%s: found %d rows for gameID %d", __func__, nRowIDs, gameID ); - for ( int ii = 0; ii < nRowIDs; ++ii ) { - feedBufferGTK( apg, rowids[ii], buf, len, from ); + if ( 0 == nRowIDs ) { + mqttc_notifyGameGone( params, &from->u.mqtt.devID, gameID ); + } else { + for ( int ii = 0; ii < nRowIDs; ++ii ) { + feedBufferGTK( apg, rowids[ii], buf, len, from ); + } } } +void +gameGoneGTK( void* closure, const CommsAddrRec* XP_UNUSED(from), XP_U32 gameID ) +{ + GtkAppGlobals* apg = (GtkAppGlobals*)closure; + gchar buf[64]; + snprintf( buf, VSIZE(buf), "game %d has been deleted on a remote device", gameID ); + gtktell( apg->window, buf ); +} + static gboolean keepalive_timer( gpointer data ) { @@ -872,6 +904,7 @@ int gtkmain( LaunchParams* params ) { GtkAppGlobals apg = {0}; + params->appGlobals = &apg; g_globals_for_signal = &apg; @@ -896,7 +929,7 @@ gtkmain( LaunchParams* params ) .msgNoticeReceived = gtkNoticeRcvd, .devIDReceived = gtkDevIDReceived, .msgErrorMsg = gtkErrorMsgRcvd, - .inviteReceived = relayInviteReceived, + .inviteReceived = relayInviteReceivedGTK, }; relaycon_init( params, &procs, &apg, @@ -904,6 +937,8 @@ gtkmain( LaunchParams* params ) params->connInfo.relay.defaultSendPort ); linux_doInitialReg( params, idIsNew ); + + mqttc_init( params ); } #ifdef XWFEATURE_SMS @@ -912,7 +947,7 @@ gtkmain( LaunchParams* params ) if ( parseSMSParams( params, &myPhone, &myPort ) ) { SMSProcs smsProcs = { .inviteReceived = smsInviteReceived, - .msgReceived = smsMsgReceivedGTK, + .msgReceived = msgReceivedGTK, }; linux_sms_init( params, myPhone, myPort, &smsProcs, &apg ); } else { @@ -933,13 +968,14 @@ gtkmain( LaunchParams* params ) } gtk_main(); - device_store( params->dutil, NULL_XWE ); + dvc_store( params->dutil, NULL_XWE ); /* closeGamesDB( params->pDb ); */ /* params->pDb = NULL; */ relaycon_cleanup( params ); #ifdef XWFEATURE_SMS linux_sms_cleanup( params ); #endif + mqttc_cleanup( params ); return 0; } /* gtkmain */ diff --git a/xwords4/linux/gtkmain.h b/xwords4/linux/gtkmain.h index b7aaa8d0a..9e5e5f903 100644 --- a/xwords4/linux/gtkmain.h +++ b/xwords4/linux/gtkmain.h @@ -27,5 +27,8 @@ int gtkmain( LaunchParams* params ); void windowDestroyed( GtkGameGlobals* globals ); void gtkOnGameSaved( void* closure, sqlite3_int64 rowid, XP_Bool firstTime ); void make_rematch( GtkAppGlobals* apg, const CommonGlobals* cGlobals ); - +void relayInviteReceivedGTK( void* closure, const NetLaunchInfo* invite ); +void msgReceivedGTK( void* closure, const CommsAddrRec* from, XP_U32 gameID, + const XP_U8* buf, XP_U16 len ); +void gameGoneGTK( void* closure, const CommsAddrRec* from, XP_U32 gameID ); #endif diff --git a/xwords4/linux/lindutil.c b/xwords4/linux/lindutil.c index 72d384db9..9071074e6 100644 --- a/xwords4/linux/lindutil.c +++ b/xwords4/linux/lindutil.c @@ -23,7 +23,11 @@ #include "linuxutl.h" #include "linuxmain.h" #include "gamesdb.h" +#include "dbgutil.h" #include "LocalizedStrIncludes.h" +#include "nli.h" +#include "cursesmain.h" +#include "gtkmain.h" static XP_U32 linux_dutil_getCurSeconds( XW_DUtilCtxt* duc, XWEnv xwe ); static const XP_UCHAR* linux_dutil_getUserString( XW_DUtilCtxt* duc, XWEnv xwe, XP_U16 code ); @@ -78,6 +82,52 @@ linux_dutil_onDupTimerChanged( XW_DUtilCtxt* XP_UNUSED(duc), XWEnv XP_UNUSED(xwe XP_LOGF( "%s(id=%d, oldVal=%d, newVal=%d)", __func__, gameID, oldVal, newVal ); } +static void +linux_dutil_onInviteReceived( XW_DUtilCtxt* duc, XWEnv XP_UNUSED(xwe), + const NetLaunchInfo* nli ) +{ + LaunchParams* params = (LaunchParams*)duc->closure; + + if ( params->useCurses ) { + CommsAddrRec addr = {0}; + nli_makeAddrRec( nli, &addr ); + inviteReceivedCurses( params->appGlobals, nli, &addr ); + } else { + relayInviteReceivedGTK( params->appGlobals, nli ); + } +} + +static void +linux_dutil_onMessageReceived( XW_DUtilCtxt* duc, XWEnv XP_UNUSED(xwe), + XP_U32 gameID, const CommsAddrRec* from, + XWStreamCtxt* stream ) +{ + XP_LOGFF( "(gameID=%d)", gameID ); + LaunchParams* params = (LaunchParams*)duc->closure; + + XP_U16 len = stream_getSize( stream ); + XP_U8 buf[len]; + stream_getBytes( stream, buf, len ); + + if ( params->useCurses ) { + mqttMsgReceivedCurses( params->appGlobals, from, gameID, buf, len ); + } else { + msgReceivedGTK( params->appGlobals, from, gameID, buf, len ); + } +} + +static void +linux_dutil_onGameGoneReceived( XW_DUtilCtxt* duc, XWEnv XP_UNUSED(xwe), + XP_U32 gameID, const CommsAddrRec* from ) +{ + LaunchParams* params = (LaunchParams*)duc->closure; + if ( params->useCurses ) { + gameGoneCurses( params->appGlobals, from, gameID ); + } else { + gameGoneGTK( params->appGlobals, from, gameID ); + } +} + XW_DUtilCtxt* dutils_init( MPFORMAL VTableMgr* vtMgr, void* closure ) { @@ -111,9 +161,14 @@ dutils_init( MPFORMAL VTableMgr* vtMgr, void* closure ) SET_PROC(notifyPause); SET_PROC(onDupTimerChanged); + SET_PROC(onInviteReceived); + SET_PROC(onMessageReceived); + SET_PROC(onGameGoneReceived); # undef SET_PROC + assertTableFull( &result->vtable, sizeof(result->vtable), "lindutil" ); + MPASSIGN( result->mpool, mpool ); return result; } diff --git a/xwords4/linux/linuxmain.c b/xwords4/linux/linuxmain.c index a53b9037d..c51c76a73 100644 --- a/xwords4/linux/linuxmain.c +++ b/xwords4/linux/linuxmain.c @@ -59,6 +59,7 @@ #include "linuxdict.h" #include "lindutil.h" #include "relaycon.h" +#include "mqttcon.h" #include "smsproto.h" #ifdef PLATFORM_NCURSES # include "cursesmain.h" @@ -299,21 +300,37 @@ linuxOpenGame( CommonGlobals* cGlobals, const TransportProcs* procs, /* If this is to be a relay connected game, tell it so. Otherwise let the invitation process and receipt of messages populate comms' addressbook */ - if ( cGlobals->gi->serverRole != SERVER_STANDALONE - && addr_hasType( ¶ms->addr, COMMS_CONN_RELAY ) ) { + if ( cGlobals->gi->serverRole != SERVER_STANDALONE ) { + if ( addr_hasType( ¶ms->addr, COMMS_CONN_RELAY ) ) { - if ( ! savedGame ) { - linuxSaveGame( cGlobals ); - savedGame = true; + if ( ! savedGame ) { + linuxSaveGame( cGlobals ); + savedGame = true; + } + CommsAddrRec addr = {0}; + comms_getInitialAddr( &addr, params->connInfo.relay.relayName, + params->connInfo.relay.defaultSendPort ); + XP_MEMCPY( addr.u.ip_relay.invite, params->connInfo.relay.invite, + 1 + XP_STRLEN(params->connInfo.relay.invite) ); + addr.u.ip_relay.seeksPublicRoom = params->connInfo.relay.seeksPublicRoom; + addr.u.ip_relay.advertiseRoom = params->connInfo.relay.advertiseRoom; + comms_augmentHostAddr( cGlobals->game.comms, NULL_XWE, &addr ); /* sends stuff */ + } + + if ( addr_hasType( ¶ms->addr, COMMS_CONN_SMS ) ) { + CommsAddrRec addr = {0}; + addr_addType( &addr, COMMS_CONN_SMS ); + XP_STRCAT( addr.u.sms.phone, params->connInfo.sms.myPhone ); + addr.u.sms.port = params->connInfo.sms.port; + comms_augmentHostAddr( cGlobals->game.comms, NULL_XWE, &addr ); + } + + if ( addr_hasType( ¶ms->addr, COMMS_CONN_MQTT ) ) { + CommsAddrRec addr = {0}; + addr_addType( &addr, COMMS_CONN_MQTT ); + addr.u.mqtt.devID = *mqttc_getDevID( params ); + comms_augmentHostAddr( cGlobals->game.comms, NULL_XWE, &addr ); } - CommsAddrRec addr = {0}; - comms_getInitialAddr( &addr, params->connInfo.relay.relayName, - params->connInfo.relay.defaultSendPort ); - XP_MEMCPY( addr.u.ip_relay.invite, params->connInfo.relay.invite, - 1 + XP_STRLEN(params->connInfo.relay.invite) ); - addr.u.ip_relay.seeksPublicRoom = params->connInfo.relay.seeksPublicRoom; - addr.u.ip_relay.advertiseRoom = params->connInfo.relay.advertiseRoom; - comms_augmentHostAddr( cGlobals->game.comms, NULL_XWE, &addr ); /* sends stuff */ } if ( !!returnAddrP ) { @@ -882,6 +899,11 @@ typedef enum { ,CMD_INVITEE_SMSNUMBER ,CMD_SMSPORT #endif + ,CMD_WITHMQTT + ,CMD_MQTTHOST + ,CMD_MQTTPORT + + ,CMD_INVITEE_MQTTDEVID ,CMD_INVITEE_COUNTS #ifdef XWFEATURE_RELAY ,CMD_ROOMNAME @@ -1018,6 +1040,10 @@ static CmdInfoRec CmdInfoRecs[] = { ,{ CMD_INVITEE_SMSNUMBER, true, "invitee-sms-number", "number to send any invitation to" } ,{ CMD_SMSPORT, true, "sms-port", "this devices's sms port" } #endif + ,{ CMD_WITHMQTT, false, "with-mqtt", "enable connecting via mqtt" } + ,{ CMD_MQTTHOST, true, "mqtt-host", "server mosquitto is running on" } + ,{ CMD_MQTTPORT, true, "mqtt-port", "port mosquitto is listening on" } + ,{ CMD_INVITEE_MQTTDEVID, true, "invitee-mqtt-devid", "upper-case hex devID to send any invitation to" } ,{ CMD_INVITEE_COUNTS, true, "invitee-counts", "When invitations sent, how many on each device? e.g. \"1:2\" for a " "three-dev game with two players on second guest" } @@ -1586,6 +1612,15 @@ linux_send( XWEnv XP_UNUSED(xwe), const XP_U8* buf, XP_U16 buflen, } break; #endif + + case COMMS_CONN_MQTT: + nSent = mqttc_send( cGlobals->params, gameID, buf, buflen, &addrRec->u.mqtt.devID ); + break; + + case COMMS_CONN_NFC: + XP_LOGFF( "I don't do nfc! Should be filtering it on invitation receipt" ); + break; + default: XP_ASSERT(0); } @@ -2587,6 +2622,8 @@ main( int argc, char** argv ) mainParams.connInfo.ip.port = DEFAULT_PORT; mainParams.connInfo.ip.hostName = "localhost"; #endif + mainParams.connInfo.mqtt.hostName = "localhost"; + mainParams.connInfo.mqtt.port = 1883; #ifdef XWFEATURE_SMS mainParams.connInfo.sms.port = 1; #endif @@ -2795,6 +2832,22 @@ main( int argc, char** argv ) addr_addType( &mainParams.addr, COMMS_CONN_SMS ); break; #endif + case CMD_WITHMQTT: + addr_addType( &mainParams.addr, COMMS_CONN_MQTT ); + break; + case CMD_MQTTHOST: + addr_addType( &mainParams.addr, COMMS_CONN_MQTT ); + mainParams.connInfo.mqtt.hostName = optarg; + break; + case CMD_MQTTPORT: + addr_addType( &mainParams.addr, COMMS_CONN_MQTT ); + mainParams.connInfo.mqtt.port = atoi(optarg); + break; + case CMD_INVITEE_MQTTDEVID: + mainParams.connInfo.mqtt.inviteeDevIDs = + g_slist_append( mainParams.connInfo.mqtt.inviteeDevIDs, optarg ); + addr_addType( &mainParams.addr, COMMS_CONN_MQTT ); + break; case CMD_DUPPACKETS: mainParams.duplicatePackets = XP_TRUE; break; diff --git a/xwords4/linux/main.h b/xwords4/linux/main.h index fe8eed4ce..89fb958ba 100644 --- a/xwords4/linux/main.h +++ b/xwords4/linux/main.h @@ -1,6 +1,6 @@ /* -*- compile-command: "make MEMDEBUG=TRUE -j3"; -*- */ /* - * Copyright 2001-2013 by Eric House (xwords@eehouse.org). All rights + * Copyright 2001 - 2020 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or @@ -51,7 +51,7 @@ typedef void (*SockReceiver)( void* closure, int socket ); typedef void (*NewSocketProc)( void* closure, int newSock, int oldSock, SockReceiver proc, void* procClosure ); -typedef struct LaunchParams { +typedef struct _LaunchParams { /* CommPipeCtxt* pipe; */ CurGameInfo pgi; @@ -67,6 +67,7 @@ typedef struct LaunchParams { XP_U32 dbFileID; #endif void* relayConStorage; /* opaque outside of relaycon.c */ + void* mqttConStorage; #ifdef XWFEATURE_SMS void* smsStorage; #endif @@ -107,7 +108,10 @@ typedef struct LaunchParams { XP_Bool skipGameOver; XP_Bool useMmap; XP_Bool closeStdin; + XP_Bool useCurses; + void* appGlobals; /* cursesmain or gtkmain sets this */ + XP_Bool useUdp; XP_Bool useHTTP; XP_Bool runSMSTest; @@ -168,6 +172,12 @@ typedef struct LaunchParams { int port; } sms; #endif + struct { + MQTTDevID devID; + GSList* inviteeDevIDs; + const char* hostName; + int port; + } mqtt; } connInfo; union { diff --git a/xwords4/linux/mqttcon.c b/xwords4/linux/mqttcon.c new file mode 100644 index 000000000..2d273694f --- /dev/null +++ b/xwords4/linux/mqttcon.c @@ -0,0 +1,275 @@ +/* -*- compile-command: "make MEMDEBUG=TRUE -j3"; -*- */ +/* + * Copyright 2020 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. + */ + +#include + +#include "mqttcon.h" +#include "gsrcwrap.h" +#include "device.h" +#include "strutils.h" + +typedef struct _MQTTConStorage { + LaunchParams* params; + struct mosquitto* mosq; + MQTTDevID clientID; + gchar clientIDStr[32]; + gchar topic[64]; + int msgPipe[2]; +} MQTTConStorage; + +#define DEFAULT_QOS 2 + +static MQTTConStorage* +getStorage( LaunchParams* params ) +{ + MQTTConStorage* storage = (MQTTConStorage*)params->mqttConStorage; + if ( NULL == storage ) { + storage = XP_CALLOC( params->mpool, sizeof(*storage) ); + params->mqttConStorage = storage; + } + return storage; +} + +static void +loadClientID( LaunchParams* params, MQTTConStorage* storage ) +{ + dvc_getMQTTDevID( params->dutil, NULL_XWE, &storage->clientID ); + formatMQTTDevID( &storage->clientID, storage->clientIDStr, + VSIZE(storage->clientIDStr) ); + formatMQTTTopic( &storage->clientID, storage->topic, VSIZE(storage->topic) ); +} + +static void +onMessageReceived( struct mosquitto *mosq, void *userdata, + const struct mosquitto_message* message ) +{ + XP_LOGFF( "(len=%d)", message->payloadlen ); + MQTTConStorage* storage = (MQTTConStorage*)userdata; + XP_ASSERT( storage->mosq == mosq ); + // XP_ASSERT( 0 == XP_STRCMP( message->topic, storage->clientID ) ); + + XP_ASSERT( message->payloadlen < 0x7FFF ); + short len = (short)message->payloadlen; + len = htons( len ); + + write( storage->msgPipe[1], &len, sizeof(len) ); + write( storage->msgPipe[1], message->payload, message->payloadlen ); +} + +static void +connect_callback( struct mosquitto *mosq, void *userdata, int result ) +{ + LOG_FUNC(); + XP_USE(mosq); + XP_USE(userdata); + XP_USE(result); + /* int i; */ + /* if(!result){ */ + /* /\* Subscribe to broker information topics on successful connect. *\/ */ + /* mosquitto_subscribe(mosq, NULL, "$SYS/#", 2); */ + /* }else{ */ + /* fprintf(stderr, "Connect failed\n"); */ + /* } */ +} + +static void +subscribe_callback( struct mosquitto *mosq, void *userdata, int mid, + int qos_count, const int *granted_qos) +{ + XP_USE(mosq); + XP_USE(userdata); + XP_USE(mid); + XP_USE(qos_count); + XP_USE(granted_qos); + XP_LOGFF ("Subscribed (mid: %d): %d", mid, granted_qos[0]); + for( int ii=1; ii < qos_count; ii++ ) { + XP_LOGFF(", %d", granted_qos[ii]); + } +} + +static void +log_callback(struct mosquitto *mosq, void *userdata, int level, const char *str ) +{ + XP_USE(mosq); + XP_USE(userdata); + XP_USE(level); + XP_LOGFF( "msg: %s", str ); +} + +static gboolean +handle_gotmsg( GIOChannel* source, GIOCondition XP_UNUSED(condition), gpointer data ) +{ + // XP_LOGFF( "(len=%d)", message->payloadlen ); + LOG_FUNC(); + MQTTConStorage* storage = (MQTTConStorage*)data; + + int pipe = g_io_channel_unix_get_fd( source ); + XP_ASSERT( pipe == storage->msgPipe[0] ); + short len; + ssize_t nRead = read( pipe, &len, sizeof(len) ); + XP_ASSERT( nRead == sizeof(len) ); + len = ntohs(len); + XP_U8 buf[len]; + nRead = read( pipe, buf, len ); + XP_ASSERT( nRead == len ); + + dvc_parseMQTTPacket( storage->params->dutil, NULL_XWE, buf, len ); + + return TRUE; +} /* handle_gotmsg */ + +static bool +postMsg( MQTTConStorage* storage, XWStreamCtxt* stream, const MQTTDevID* invitee ) +{ + const XP_U8* bytes = stream_getPtr( stream ); + XP_U16 len = stream_getSize( stream ); + + int mid; + +#ifdef DEBUG + XP_UCHAR* sum = dutil_md5sum( storage->params->dutil, NULL_XWE, bytes, len ); + XP_LOGFF( "sending %d bytes with sum %s", len, sum ); + XP_FREEP( storage->params->mpool, &sum ); +#endif + + gchar buf[32]; + int err = mosquitto_publish( storage->mosq, &mid, + formatMQTTTopic( invitee, buf, sizeof(buf) ), + len, bytes, DEFAULT_QOS, false ); + XP_LOGFF( "mosquitto_publish(topic=%s) => %s; mid=%d", buf, mosquitto_strerror(err), mid ); + + stream_destroy( stream, NULL_XWE ); + return err == 0; +} + +void +mqttc_init( LaunchParams* params ) +{ + + XP_ASSERT( !params->mqttConStorage ); + MQTTConStorage* storage = getStorage( params ); + storage->params = params; + + loadClientID( params, storage ); + + int res = pipe( storage->msgPipe ); + XP_ASSERT( !res ); + ADD_SOCKET( storage, storage->msgPipe[0], handle_gotmsg ); + + int err = mosquitto_lib_init(); + XP_LOGFF( "mosquitto_lib_init() => %d", err ); + XP_ASSERT( 0 == err ); + + bool cleanSession = false; + struct mosquitto* mosq = storage->mosq = + mosquitto_new( storage->clientIDStr, cleanSession, storage ); + + mosquitto_log_callback_set( mosq, log_callback ); + mosquitto_connect_callback_set( mosq, connect_callback ); + mosquitto_message_callback_set( mosq, onMessageReceived ); + mosquitto_subscribe_callback_set( mosq, subscribe_callback ); + + int keepalive = 60; + err = mosquitto_connect( mosq, params->connInfo.mqtt.hostName, + params->connInfo.mqtt.port, keepalive ); + XP_LOGFF( "mosquitto_connect() => %s", mosquitto_strerror(err) ); + if ( MOSQ_ERR_SUCCESS == err ) { + int mid; + err = mosquitto_subscribe( mosq, &mid, storage->topic, DEFAULT_QOS ); + XP_LOGFF( "mosquitto_subscribe(topic=%s) => %s, mid=%d", storage->topic, + mosquitto_strerror(err), mid ); + + err = mosquitto_loop_start( mosq ); + XP_ASSERT( !err ); + } else { + XP_LOGFF( "failed to connect so not proceeding" ); + } +} + +void +mqttc_cleanup( LaunchParams* params ) +{ + MQTTConStorage* storage = getStorage( params ); + + int err = mosquitto_loop_stop( storage->mosq, true ); /* blocks until thread dies */ + XP_LOGFF( "mosquitto_loop_stop() => %s", mosquitto_strerror(err) ); + mosquitto_destroy( storage->mosq ); + storage->mosq = NULL; + mosquitto_lib_cleanup(); + + XP_ASSERT( params->mqttConStorage == storage ); /* cheat */ + XP_FREEP( params->mpool, &storage ); + params->mqttConStorage = NULL; +} + +const MQTTDevID* +mqttc_getDevID( LaunchParams* params ) +{ + MQTTConStorage* storage = getStorage( params ); + return &storage->clientID; +} + +const gchar* +mqttc_getDevIDStr( LaunchParams* params ) +{ + MQTTConStorage* storage = getStorage( params ); + return storage->clientIDStr; +} + +void +mqttc_invite( LaunchParams* params, NetLaunchInfo* nli, const MQTTDevID* invitee ) +{ + MQTTConStorage* storage = getStorage( params ); + gchar buf[32]; + XP_LOGFF( "need to send to %s", formatMQTTDevID(invitee, buf, sizeof(buf) ) ); + + XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(params->mpool) + params->vtMgr ); + + dvc_makeMQTTInvite( stream, nli ); + + postMsg( storage, stream, invitee ); +} + +XP_S16 +mqttc_send( LaunchParams* params, XP_U32 gameID, const XP_U8* buf, + XP_U16 len, const MQTTDevID* addressee ) +{ + XP_S16 result = -1; + MQTTConStorage* storage = getStorage( params ); + XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(params->mpool) + params->vtMgr ); + + dvc_makeMQTTMessage( params->dutil, NULL_XWE, stream, + gameID, buf, len ); + if ( postMsg( storage, stream, addressee ) ) { + result = len; + } + return result; +} + +void +mqttc_notifyGameGone( LaunchParams* params, const MQTTDevID* addressee, XP_U32 gameID ) +{ + MQTTConStorage* storage = getStorage( params ); + XWStreamCtxt* stream = mem_stream_make_raw( MPPARM(params->mpool) + params->vtMgr ); + dvc_makeMQTTNoSuchGame( params->dutil, NULL_XWE, stream, gameID ); + postMsg( storage, stream, addressee ); +} diff --git a/xwords4/linux/mqttcon.h b/xwords4/linux/mqttcon.h new file mode 100644 index 000000000..46c707712 --- /dev/null +++ b/xwords4/linux/mqttcon.h @@ -0,0 +1,38 @@ +/* -*- compile-command: "make MEMDEBUG=TRUE -j3"; -*- */ +/* + * Copyright 2020 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. + */ + +#ifndef _MQTTCON_H_ +#define _MQTTCON_H_ + +#include "main.h" +#include "nli.h" + +void mqttc_init( LaunchParams* params ); +void mqttc_cleanup( LaunchParams* params ); + +const MQTTDevID* mqttc_getDevID( LaunchParams* params ); +const gchar* mqttc_getDevIDStr( LaunchParams* params ); +void mqttc_invite( LaunchParams* params, NetLaunchInfo* nli, const MQTTDevID* mqttInvitee ); +XP_S16 mqttc_send( LaunchParams* params, XP_U32 gameID, const XP_U8* buf, XP_U16 len, + const MQTTDevID* addressee ); +void mqttc_notifyGameGone( LaunchParams* params, const MQTTDevID* addressee, XP_U32 gameID ); + +bool mqttc_strToDevID( const gchar* str, MQTTDevID* result ); + +#endif diff --git a/xwords4/linux/relaycon.h b/xwords4/linux/relaycon.h index 46d3d0020..7719a0f61 100644 --- a/xwords4/linux/relaycon.h +++ b/xwords4/linux/relaycon.h @@ -33,7 +33,7 @@ typedef struct _Procs { void (*devIDReceived)( void* closure, const XP_UCHAR* devID, XP_U16 maxInterval ); void (*msgErrorMsg)( void* closure, const XP_UCHAR* msg ); - void (*inviteReceived)( void* closure, NetLaunchInfo* invit ); + void (*inviteReceived)( void* closure, const NetLaunchInfo* invit ); } RelayConnProcs; void relaycon_init( LaunchParams* params, const RelayConnProcs* procs, diff --git a/xwords4/linux/scripts/discon_ok2.py b/xwords4/linux/scripts/discon_ok2.py index 53b7a020d..370a8be80 100755 --- a/xwords4/linux/scripts/discon_ok2.py +++ b/xwords4/linux/scripts/discon_ok2.py @@ -165,6 +165,7 @@ class Device(): sTilesLeftTrayPat = re.compile('.*player \d+ now has (\d+) tiles') sRelayIDPat = re.compile('.*UPDATE games.*seed=(\d+),.*relayid=\'([^\']+)\'.*') sDevIDPat = re.compile('.*storing new devid: ([\da-fA-F]+).*') + sMQTTDevIDPat = re.compile('.*dvc_getMQTTDevID.*: generated id: ([\d[A-F]+).*') sConnPat = re.compile('.*linux_util_informMissing\(isServer.*nMissing=0\).*') sScoresDup = [] @@ -198,6 +199,8 @@ class Device(): self.relayID = None self.inviteeDevID = None self.inviteeDevIDs = [] # only servers use this + self.inviteeMQTTDevID = None + self.inviteeMQTTDevIDs = [] self.connected = False self.relaySeed = 0 self.locked = False @@ -261,6 +264,12 @@ class Device(): match = Device.sDevIDPat.match(line) if match: self.inviteeDevID = int(match.group(1), 16) + if self.args.ADD_MQTT and not self.inviteeMQTTDevID: + match = Device.sMQTTDevIDPat.match(line) + if match: + self.inviteeMQTTDevID = int(match.group(1), 16) + # print('read mqtt devid: {:16X}'.format(self.inviteeMQTTDevID)) + if not self.connected: match = Device.sConnPat.match(line) if match: self.connected = True @@ -308,6 +317,18 @@ class Device(): for inviteeDevID in self.inviteeDevIDs: args += ['--invitee-relayid', str(inviteeDevID)] + if self.args.ADD_MQTT: + if self.order == 1 and not self.connected: + for peer in self.peers: + if peer.inviteeMQTTDevID and not peer == self: + if not peer.inviteeMQTTDevID in self.inviteeMQTTDevIDs: + self.inviteeMQTTDevIDs.append(peer.inviteeMQTTDevID) + if self.inviteeMQTTDevIDs: + args += [ '--force-invite' ] + for idid in self.inviteeMQTTDevIDs: + asHexStr = '{:16X}'.format(idid) + args += ['--invitee-mqtt-devid', asHexStr] + self.proc = subprocess.Popen(args, stdout = subprocess.DEVNULL, stderr = subprocess.PIPE, universal_newlines = True) self.pid = self.proc.pid @@ -450,6 +471,11 @@ def build_cmds(args): for dev in range(2, NDEVS + 1): PARAMS += [ '--invitee-sms-number', makeSMSPhoneNo(GAME, dev) ] + if args.ADD_MQTT: + PARAMS += [ '--mqtt-port', args.MQTT_PORT, '--mqtt-host', args.MQTT_HOST ] + if DEV == 1: + PARAMS += [ '--force-invite' ] + if args.UNDO_PCT > 0: PARAMS += ['--undo-pct', args.UNDO_PCT] PARAMS += [ '--game-dict', DICT] @@ -755,6 +781,11 @@ def mkParser(): parser.add_argument('--add-sms', dest = 'ADD_SMS', default = False, action = 'store_true') parser.add_argument('--sms-fail-pct', dest = 'SMS_FAIL_PCT', default = 0, type = int) + + parser.add_argument('--add-mqtt', dest = 'ADD_MQTT', default = False, action = 'store_true') + parser.add_argument('--mqtt-port', dest = 'MQTT_PORT', default = 1883 ) + parser.add_argument('--mqtt-host', dest = 'MQTT_HOST', default = 'localhost' ) + parser.add_argument('--remove-relay', dest = 'ADD_RELAY', default = True, action = 'store_false') parser.add_argument('--core-pat', dest = 'CORE_PAT', default = os.environ.get('DISCON_COREPAT'), diff --git a/xwords4/relay/scripts/mqtt-showinplay.py b/xwords4/relay/scripts/mqtt-showinplay.py new file mode 100755 index 000000000..25ec50939 --- /dev/null +++ b/xwords4/relay/scripts/mqtt-showinplay.py @@ -0,0 +1,65 @@ +#!/usr/bin/python3 + +import argparse, re +import paho.mqtt.client as mqtt + +g_topics = [ + '$SYS/broker/clients/disconnected', + # '$SYS/broker/+/+', + 'xw4/device/#', + ] + +sDevIDPat = re.compile('xw4/device/([\dA-F]+)') + +# Define event callbacks +def on_connect(client, userdata, flags, rc): + print("rc: " + str(rc)) + +def on_message(client, obj, msg): + match = sDevIDPat.match(msg.topic) + if match: + print('for: {}, len: {}'.format(match.group(1), len(msg.payload))) + +def on_publish(client, obj, mid): + print("mid: " + str(mid)) + +def on_subscribe(client, obj, mid, granted_qos): + print("Subscribed: " + str(mid) + " " + str(granted_qos)) + +def on_log(client, obj, level, string): + print(string) + +def makeClient(): + mqttc = mqtt.Client() + # Assign event callbacks + mqttc.on_message = on_message + mqttc.on_connect = on_connect + mqttc.on_publish = on_publish + mqttc.on_subscribe = on_subscribe + return mqttc + +def mkParser(): + parser = argparse.ArgumentParser() + parser.add_argument('--host', dest = 'HOST', default = 'liquidsugar.net', + help = 'the host mosquitto is on') + parser.add_argument('--port', dest = 'PORT', default = 1883, + help = 'the port mosquitto is on') + return parser + +def main(): + args = mkParser().parse_args() + + mqttc = makeClient() + mqttc.connect(args.HOST, args.PORT) + # Start subscribe, with QoS level 2 + for topic in g_topics: + mqttc.subscribe(topic, 2) + while True: + err = mqttc.loop() + if 0 != err: + print('got {} from loop()'.format(err)) + break + +############################################################################## +if __name__ == '__main__': + main()