From 2a54f6f1769387fb2964c9bd917a38a243f369d9 Mon Sep 17 00:00:00 2001 From: Eric House Date: Sat, 14 Dec 2019 10:55:05 -0800 Subject: [PATCH 01/24] cleanup: make linux more like android Wasn't using the game_receiveMessage() utility --- xwords4/common/game.c | 3 ++- xwords4/common/game.h | 2 +- xwords4/linux/linuxmain.c | 29 ++++------------------------- 3 files changed, 7 insertions(+), 27 deletions(-) diff --git a/xwords4/common/game.c b/xwords4/common/game.c index 78e4f0a35..1facc227f 100644 --- a/xwords4/common/game.c +++ b/xwords4/common/game.c @@ -339,7 +339,8 @@ game_saveSucceeded( const XWGame* game, XP_U16 saveToken ) } XP_Bool -game_receiveMessage( XWGame* game, XWStreamCtxt* stream, CommsAddrRec* retAddr ) +game_receiveMessage( XWGame* game, XWStreamCtxt* stream, + const CommsAddrRec* retAddr ) { ServerCtxt* server = game->server; CommsMsgState commsState; diff --git a/xwords4/common/game.h b/xwords4/common/game.h index caeea7140..10d4d7187 100644 --- a/xwords4/common/game.h +++ b/xwords4/common/game.h @@ -84,7 +84,7 @@ void game_saveToStream( const XWGame* game, const CurGameInfo* gi, void game_saveSucceeded( const XWGame* game, XP_U16 saveToken ); XP_Bool game_receiveMessage( XWGame* game, XWStreamCtxt* stream, - CommsAddrRec* retAddr ); + const CommsAddrRec* retAddr ); void game_dispose( XWGame* game ); diff --git a/xwords4/linux/linuxmain.c b/xwords4/linux/linuxmain.c index c39445e2e..38555c936 100644 --- a/xwords4/linux/linuxmain.c +++ b/xwords4/linux/linuxmain.c @@ -178,27 +178,6 @@ makeDictForStream( CommonGlobals* cGlobals, XWStreamCtxt* stream ) return dict; } -static XP_Bool -processMessage( CommonGlobals* cGlobals, XWStreamCtxt* stream, - const CommsAddrRec* from, XP_Bool doDo ) -{ - XWGame* game = &cGlobals->game; - CommsMsgState state; - XP_Bool received = comms_checkIncomingStream( game->comms, stream, from, &state ); - XP_Bool draw = received && server_receiveMessage( game->server, stream ); - comms_msgProcessed( game->comms, &state, !draw ); - - if ( doDo && draw ) { - ServerCtxt* server = cGlobals->game.server; - XP_U16 ii; - for ( ii = 0; ii < 5; ++ii ) { - (void)server_do( server ); - } - } - - return draw; -} - void gameGotBuf( CommonGlobals* cGlobals, XP_Bool hasDraw, const XP_U8* buf, XP_U16 len, const CommsAddrRec* from ) @@ -208,7 +187,7 @@ gameGotBuf( CommonGlobals* cGlobals, XP_Bool hasDraw, const XP_U8* buf, XWGame* game = &cGlobals->game; XWStreamCtxt* stream = stream_from_msgbuf( cGlobals, buf, len ); if ( !!stream ) { - redraw = processMessage( cGlobals, stream, from, XP_FALSE ); + redraw = game_receiveMessage( game, stream, from ); if ( redraw ) { saveGame( cGlobals ); } @@ -433,7 +412,7 @@ handle_messages_from( CommonGlobals* cGlobals, const TransportProcs* procs, stream = mem_stream_make_raw( MPPARM(cGlobals->util->mpool) params->vtMgr ); stream_putBytes( stream, buf, len ); - (void)processMessage( cGlobals, stream, NULL, XP_TRUE ); + (void)game_receiveMessage( &cGlobals->game, stream, NULL ); stream_destroy( stream ); } @@ -483,7 +462,7 @@ read_pipe_then_close( CommonGlobals* cGlobals, const TransportProcs* procs ) stream = mem_stream_make_raw( MPPARM(cGlobals->util->mpool) params->vtMgr ); stream_putBytes( stream, buf, len ); - (void)processMessage( cGlobals, stream, NULL, XP_TRUE ); + (void)game_receiveMessage( &cGlobals->game, stream, NULL ); stream_destroy( stream ); } @@ -1162,7 +1141,7 @@ linux_relay_ioproc( GIOChannel* source, GIOCondition condition, gpointer data ) CommsAddrRec addr = {0}; addr_addType( &addr, COMMS_CONN_RELAY ); - redraw = processMessage( cGlobals, inboundS, &addr, XP_FALSE ); + redraw = game_receiveMessage( &cGlobals->game, inboundS, &addr ); stream_destroy( inboundS ); } From 764cefcddd15ae797c25d71ae0595834541f03fa Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 16 Dec 2019 06:43:01 -0800 Subject: [PATCH 02/24] debug only: show pending msg count in game list item --- .../main/java/org/eehouse/android/xw4/GameListItem.java | 8 +++++++- .../android/app/src/main/res/layout/game_list_item.xml | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java index e3483b78f..5d8b342da 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java @@ -33,7 +33,6 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; - import org.eehouse.android.xw4.jni.GameSummary; import org.eehouse.android.xw4.jni.JNIThread; import org.eehouse.android.xw4.loc.LocUtils; @@ -336,6 +335,13 @@ public class GameListItem extends LinearLayout findViewById( R.id.has_chat_marker ) .setVisibility( hasChat ? View.VISIBLE : View.GONE ); + if ( BuildConfig.DEBUG ) { + TextView tv = (TextView)findViewById( R.id.n_pending ); + int nPending = summary.nPacketsPending; + String str = nPending == 0 ? "" : String.format( "%d", nPending ); + tv.setText( str ); + } + String roleSummary = summary.summarizeRole( m_context, m_rowid ); m_role.setVisibility( null == roleSummary ? View.GONE : View.VISIBLE ); if ( null != roleSummary ) { diff --git a/xwords4/android/app/src/main/res/layout/game_list_item.xml b/xwords4/android/app/src/main/res/layout/game_list_item.xml index 49e924837..1c3ee0231 100644 --- a/xwords4/android/app/src/main/res/layout/game_list_item.xml +++ b/xwords4/android/app/src/main/res/layout/game_list_item.xml @@ -38,6 +38,11 @@ android:paddingLeft="8dip" android:paddingRight="8dip" > + + Date: Wed, 11 Dec 2019 11:27:17 +0000 Subject: [PATCH 03/24] Translated using Weblate (Spanish) Currently translated at 3.2% (25 of 778 strings) --- xwords4/android/res_src/values-es/strings.xml | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/xwords4/android/res_src/values-es/strings.xml b/xwords4/android/res_src/values-es/strings.xml index a6b3daec9..b1e9f10f5 100644 --- a/xwords4/android/res_src/values-es/strings.xml +++ b/xwords4/android/res_src/values-es/strings.xml @@ -1,2 +1,50 @@ - \ No newline at end of file + + Añadir grupo + %1$s (%2$s) + %1$s (robot) + (estableciendo conexión…) + Configurando para entrar en la sala \"%1$s\" + Esperando jugadores en la sala \"%1$s\" + Partida en curso en la sala \"%1$s\" + Partida finalizada en la sala \"%1$s\" + Jugadores invitados + Jugadores invitados en la sala \"%1$s\" + Fin de la partida + + %1$d movimiento realizado + %1$d movimientos realizados + + Eliminar + Reiniciar + Listas de palabras… + Ajustes de la aplicación… + Comprobar si hay jugadas + Comprobando en el repetidor si hay movimientos, etc… + No se han encontrado partidas conectadas a través del repetidor. + Acerca de CrossWords… + Ajustes del juego… + Cambiar nombre… + Mover al grupo… + Borrar partida + Reiniciar + Crear nuevo desde + Copiar + Cambiar nombre de la partida + Cambiar nombre de la partida a: + Cambiar el nombre de esta partida (solo en este dispositivo) por: + + ¿Estás seguro de que quieres eliminar esta partida\? No podrás deshacer esta acción. + ¿Estás seguro de que quieres eliminar %1$d las siguientes partidas\? No podrás deshacer esta acción. + + + ¿Seguro que quieres reiniciar esta partida\? +\n +\n(La opción de reiniciar borrará todos los movimientos y cualquier información de conexión). + ¿Seguro que quieres reiniciar las %1$d siguientes partidas\? +\n +\n(La opción de reiniciar borrará todos los movimientos y cualquier información de conexión). + + Listas de palabras de CrossWords + Descargar más… + \ No newline at end of file From cdd162b4c721d421111f3c1e4dea893f7a7ca33f Mon Sep 17 00:00:00 2001 From: Andrea Hernandiz Lopez Date: Mon, 16 Dec 2019 12:26:58 +0000 Subject: [PATCH 04/24] Translated using Weblate (Spanish) Currently translated at 9.3% (72 of 778 strings) --- xwords4/android/res_src/values-es/strings.xml | 66 +++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/xwords4/android/res_src/values-es/strings.xml b/xwords4/android/res_src/values-es/strings.xml index b1e9f10f5..c2114f53e 100644 --- a/xwords4/android/res_src/values-es/strings.xml +++ b/xwords4/android/res_src/values-es/strings.xml @@ -15,13 +15,13 @@ %1$d movimiento realizado %1$d movimientos realizados - Eliminar + Borrar Reiniciar Listas de palabras… Ajustes de la aplicación… Comprobar si hay jugadas - Comprobando en el repetidor si hay movimientos, etc… - No se han encontrado partidas conectadas a través del repetidor. + Comprobando movimientos, etc… en el repetidor + No se han encontrado partidas conectadas en el repetidor. Acerca de CrossWords… Ajustes del juego… Cambiar nombre… @@ -31,7 +31,7 @@ Crear nuevo desde Copiar Cambiar nombre de la partida - Cambiar nombre de la partida a: + Cambiar nombre de la partida por: Cambiar el nombre de esta partida (solo en este dispositivo) por: ¿Estás seguro de que quieres eliminar esta partida\? No podrás deshacer esta acción. @@ -47,4 +47,62 @@ Listas de palabras de CrossWords Descargar más… + Conexiones… + Las partidas que ya se han conectado al repetidor no se pueden copiar. Utiliza \"Crear nuevo desde\" para jugar con una copia con los mismos ajustes. + Listas predefinidas + Descargas + Cambiar ubicación de almacenamiento + Establecer por defecto + + ¿Estás seguro de que quieres eliminar la lista de palabras %1$s\? + ¿Estás seguro de que quieres eliminar las listas de palabras %1$s\? + + Si eliminas %1$s te quedarás sin listas de palabras %2$s . No podrás abrir una o más partidas (hasta que no descargues una lista que la sustituya.) + Para que jugadores la lista de palabras %1$s debería ser la predeterminada para nuevas partidas\? (El idioma %2$s será el predeterminado para ambos.) + Jugador + Robot + Ambos + Ubicación de la lista de palabras %1$s + Interna + Externa + Ajustes de %1$s + Ajustes de %1$s (en red) + Idiomas (basado en las listas de palabras instaladas) + Ajustes de bloqueo + Jugadores (pulsa para editar) + Jugadores (%1$d conectados, %2$d no conectados) + Añadir jugador + Barajar jugadores + Idioma de la partida + Idioma/lista de palabras de la partida + Conexión (vía %1$s) + Unirse a una sala pública + Nombre de la sala + Convertir en sala pública + Seleccionar sala pública + Buscando salas públicas para %1$d-jugadores en %2$s. + No se han encontrados salas públicas para %1$d-jugadores en %2$s. Inténtalo de nuevo o crea una propia . + Otros ajustes + Mostrar pistas + Mostrar pistas (en red) + Activar cronómetro de juego + Minutos por jugador + Elige el nivel de inteligencia del robot + Robot inteligente + Normal + Avanzado + Cómo manejar las palabras falsas (palabras que no están en la lista) + Ignorar las palabras falsas + Advertir si hay palabras falsas + Rechazar palabras falsas + Al menos un jugador debe marcarse como \"Remoto\" en una partida iniciada como Host. + (Jugador externo) + Esta partida está en curso. Si guardas estos cambios se reiniciará. ¿Quieres guardar los cambios\? + Confirmar cambios + Jugar + Jugador[es] externo[s] + En una partida multidispositivo debe haber al menos un jugador de este dispositivo y otro que no lo sea. Por favor, busca jugadores externos. + Editar jugador + Jugador externo + Nombre: \ No newline at end of file From 7b53c48a7c4cf84c65dfc1d095b23a93068fe857 Mon Sep 17 00:00:00 2001 From: Andrea Hernandiz Lopez Date: Wed, 18 Dec 2019 15:53:32 +0000 Subject: [PATCH 05/24] Translated using Weblate (Spanish) Currently translated at 11.4% (89 of 778 strings) --- xwords4/android/res_src/values-es/strings.xml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/xwords4/android/res_src/values-es/strings.xml b/xwords4/android/res_src/values-es/strings.xml index c2114f53e..6a8a25419 100644 --- a/xwords4/android/res_src/values-es/strings.xml +++ b/xwords4/android/res_src/values-es/strings.xml @@ -105,4 +105,29 @@ Editar jugador Jugador externo Nombre: + Lista de palabras (en %1$s) + Listas de palabras instaladas (en %1$s) + Jugador robot + Contraseña + Validar intercambio + Cancelar intercambio + Pulsa en las fichas para seleccionarlas… + 2L + 2P + 3L + 3P + (Todavía no hay movimientos) + + "La partida está esperando un jugador remoto. Si aún no lo has hecho, ¿te gustaría invitar a alguien a unirse\?" + "La partida está esperando %1$d jugadores remotos. Si aún no lo has hecho, ¿te gustaría invitar a alguien a unirse\?" + + + Ya has invitado a un jugador remoto a esta partida. Estamos esperando su respuesta. Por favor, pulsa el botón \"invitar de nuevo\" si crees que la invitación no se ha enviado. + Ya has enviado %1$d invitaciones para esta partida. Estamos esperando sus respuestas. Por favor, pulsa el botón \"invitar de nuevo\" si crees que las invitaciones no se han enviado. + + Esta partida ha sido creada a partir de una invitación que has recibido. La partida comenzará tan pronto como se pueda conectar con el remitente y cualquier otro invitado. + O simplemente pulsa para invitar -- si el otro dispositivo tiene también Android Beaming y está cerca. + " (Estás esperando a varios jugadores remotos. No tienes que invitarlos a todos a la vez, pero esta alerta no será descartada hasta que todos hayan sido invitados y las invitaciones aceptadas.)" + Invitación enviada por SMS al número de teléfono %1$s a %2$s + Invitación enviada por SMS a %1$s en %2$s \ No newline at end of file From 47d4d51876d8b4f7a3ce9481c3760b434b548543 Mon Sep 17 00:00:00 2001 From: Eric House Date: Wed, 18 Dec 2019 09:34:25 -0800 Subject: [PATCH 06/24] pull in stream changes from dup branch No impact now, but I want to change the game stream format to add deviceID somewhere and that won't merge with these other changes. --- xwords4/common/comtypes.h | 3 ++- xwords4/common/game.c | 8 +++++++- xwords4/common/gameinfo.h | 1 + xwords4/common/server.c | 25 ++++++++++++++++++++++++- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/xwords4/common/comtypes.h b/xwords4/common/comtypes.h index b36d99994..42130e2ae 100644 --- a/xwords4/common/comtypes.h +++ b/xwords4/common/comtypes.h @@ -47,6 +47,7 @@ #endif #define MAX_COLS MAX_ROWS +#define STREAM_VERS_DUPLICATE 0x1B #define STREAM_VERS_DISABLEDS 0x1A #define STREAM_VERS_DEVIDS 0x19 #define STREAM_VERS_MULTIADDR 0x18 @@ -88,7 +89,7 @@ #define STREAM_VERS_405 0x01 /* search for FIX_NEXT_VERSION_CHANGE next time this is changed */ -#define CUR_STREAM_VERS STREAM_VERS_DISABLEDS +#define CUR_STREAM_VERS STREAM_VERS_DUPLICATE typedef struct XP_Rect { XP_S16 left; diff --git a/xwords4/common/game.c b/xwords4/common/game.c index 1facc227f..187a6e1c7 100644 --- a/xwords4/common/game.c +++ b/xwords4/common/game.c @@ -487,7 +487,9 @@ gi_copy( MPFORMAL CurGameInfo* destGI, const CurGameInfo* srcGI ) destGI->phoniesAction = srcGI->phoniesAction; destGI->allowPickTiles = srcGI->allowPickTiles; destGI->forceChannel = srcGI->forceChannel; - XP_LOGF( "%s: copied forceChannel: %d", __func__, destGI->forceChannel ); + destGI->inDuplicateMode = srcGI->inDuplicateMode; + XP_LOGF( "%s: copied forceChannel: %d; inDuplicateMode: %d", __func__, + destGI->forceChannel, destGI->inDuplicateMode ); for ( srcPl = srcGI->players, destPl = destGI->players, ii = 0; ii < nPlayers; ++srcPl, ++destPl, ++ii ) { @@ -566,6 +568,9 @@ gi_readFromStream( MPFORMAL XWStreamCtxt* stream, CurGameInfo* gi ) gi->phoniesAction = (XWPhoniesChoice)stream_getBits( stream, 2 ); gi->timerEnabled = stream_getBits( stream, 1 ); + gi->inDuplicateMode = strVersion >= STREAM_VERS_DUPLICATE + ? stream_getBits( stream, 1 ) + : XP_FALSE; if ( strVersion >= STREAM_VERS_41B4 ) { gi->allowPickTiles = stream_getBits( stream, 1 ); gi->allowHintRect = stream_getBits( stream, 1 ); @@ -641,6 +646,7 @@ gi_writeToStream( XWStreamCtxt* stream, const CurGameInfo* gi ) stream_putBits( stream, 1, gi->hintsNotAllowed ); stream_putBits( stream, 2, gi->phoniesAction ); stream_putBits( stream, 1, gi->timerEnabled ); + stream_putBits( stream, 1, gi->inDuplicateMode ); stream_putBits( stream, 1, gi->allowPickTiles ); stream_putBits( stream, 1, gi->allowHintRect ); stream_putBits( stream, 1, gi->confirmBTConnect ); diff --git a/xwords4/common/gameinfo.h b/xwords4/common/gameinfo.h index aa931bc5b..608b7c059 100644 --- a/xwords4/common/gameinfo.h +++ b/xwords4/common/gameinfo.h @@ -58,6 +58,7 @@ typedef struct CurGameInfo { XP_Bool timerEnabled; XP_Bool allowPickTiles; XP_Bool allowHintRect; + XP_Bool inDuplicateMode; XWPhoniesChoice phoniesAction; XP_Bool confirmBTConnect; /* only used for BT */ } CurGameInfo; diff --git a/xwords4/common/server.c b/xwords4/common/server.c index 325f3b38a..85d5404dd 100644 --- a/xwords4/common/server.c +++ b/xwords4/common/server.c @@ -1,6 +1,6 @@ /* -*- compile-command: "cd ../linux && make -j3 MEMDEBUG=TRUE"; -*- */ /* - * Copyright 1997-2009 by Eric House (xwords@eehouse.org). All rights + * Copyright 1997 - 2019 by Eric House (xwords@eehouse.org). All rights * reserved. * * This program is free software; you can redistribute it and/or @@ -84,6 +84,7 @@ typedef struct ServerVolatiles { typedef struct ServerNonvolatiles { XP_U32 lastMoveTime; /* seconds of last turn change */ + XP_S32 dupTimerExpires; XP_U8 nDevices; XW_State gameState; XW_State stateAfterShow; @@ -103,6 +104,9 @@ typedef struct ServerNonvolatiles { RemoteAddress addresses[MAX_NUM_PLAYERS]; XWStreamCtxt* prevMoveStream; /* save it to print later */ XWStreamCtxt* prevWordsStream; + XP_Bool dupTurnsMade[MAX_NUM_PLAYERS]; + XP_Bool dupTurnsForced[MAX_NUM_PLAYERS]; + XP_Bool dupTurnsSent; /* used on client only */ } ServerNonvolatiles; struct ServerCtxt { @@ -290,6 +294,9 @@ getNV( XWStreamCtxt* stream, ServerNonvolatiles* nv, XP_U16 nPlayers ) if ( STREAM_VERS_DICTNAME <= version ) { nv->lastMoveTime = stream_getU32( stream ); } + if ( STREAM_VERS_DUPLICATE <= version ) { + nv->dupTimerExpires = stream_getU32( stream ); + } if ( version < STREAM_VERS_SERVER_SAVES_TOSHOW ) { /* no longer used */ @@ -327,6 +334,15 @@ getNV( XWStreamCtxt* stream, ServerNonvolatiles* nv, XP_U16 nPlayers ) } /* XP_LOGF( "%s: read streamVersion: 0x%x", __func__, nv->streamVersion ); */ #endif + + if ( version >= STREAM_VERS_DUPLICATE ) { + for ( ii = 0; ii < nPlayers; ++ii ) { + nv->dupTurnsMade[ii] = stream_getBits( stream, 1 ); + XP_LOGF( "%s(): dupTurnsMade[%d]: %d", __func__, ii, nv->dupTurnsMade[ii] ); + nv->dupTurnsForced[ii] = stream_getBits( stream, 1 ); + } + nv->dupTurnsSent = stream_getBits( stream, 1 ); + } } /* getNV */ static void @@ -335,6 +351,7 @@ putNV( XWStreamCtxt* stream, const ServerNonvolatiles* nv, XP_U16 nPlayers ) XP_U16 ii; stream_putU32( stream, nv->lastMoveTime ); + stream_putU32( stream, nv->dupTimerExpires ); /* number of players is upper limit on device count */ stream_putBits( stream, NDEVICES_NBITS, nv->nDevices-1 ); @@ -358,6 +375,12 @@ putNV( XWStreamCtxt* stream, const ServerNonvolatiles* nv, XP_U16 nPlayers ) stream_putU8( stream, nv->streamVersion ); /* XP_LOGF( "%s: wrote streamVersion: 0x%x", __func__, nv->streamVersion ); */ #endif + + for ( ii = 0; ii < nPlayers; ++ii ) { + stream_putBits( stream, 1, nv->dupTurnsMade[ii] ); + stream_putBits( stream, 1, nv->dupTurnsForced[ii] ); + } + stream_putBits( stream, 1, nv->dupTurnsSent ); } /* putNV */ static XWStreamCtxt* From 7b309211908dc11a2e475a25a8e2237df8960e95 Mon Sep 17 00:00:00 2001 From: Eric House Date: Fri, 20 Dec 2019 18:39:36 -0800 Subject: [PATCH 07/24] fix ClassCastException, and fix moving to new group (Fixing the latter fixed the former.) --- .../eehouse/android/xw4/GamesListDelegate.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 e9e3507f2..6c66aebc3 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 @@ -109,7 +109,7 @@ public class GamesListDelegate extends ListDelegateBase } int groupSelItem; boolean nextIsSolo; - boolean moveAfterNewGroup; + long[] moveAfterNewGroup; Set selGames; Set selGroupIDs; } @@ -818,7 +818,7 @@ public class GamesListDelegate extends ListDelegateBase @Override public void onClick( DialogInterface dlg, int item ) { - m_mySIS.moveAfterNewGroup = true; + m_mySIS.moveAfterNewGroup = games; showDialogFragment( DlgID.NEW_GROUP ); } } ) @@ -1596,7 +1596,7 @@ public class GamesListDelegate extends ListDelegateBase break; case R.id.games_menu_newgroup: - m_mySIS.moveAfterNewGroup = false; + m_mySIS.moveAfterNewGroup = null; showDialogFragment( DlgID.NEW_GROUP ); break; @@ -2448,11 +2448,10 @@ public class GamesListDelegate extends ListDelegateBase private void showNewGroupIf() { - if ( m_mySIS.moveAfterNewGroup ) { - m_mySIS.moveAfterNewGroup = false; - Long[] games = m_mySIS.selGames - .toArray( new Long[m_mySIS.selGames.size()] ); - showDialogFragment( DlgID.CHANGE_GROUP, (Object)games ); + long[] games = m_mySIS.moveAfterNewGroup; + if ( null != games ) { + m_mySIS.moveAfterNewGroup = null; + showDialogFragment( DlgID.CHANGE_GROUP, games ); } } From dae24f55d481285d424e96edf24d8998790dd98b Mon Sep 17 00:00:00 2001 From: Eric House Date: Fri, 20 Dec 2019 19:09:47 -0800 Subject: [PATCH 08/24] fix reported NPE: try 100ms to get lock then give up I suspect it's really rare to fail to get the lock when the game's not open, but hope that trying 100ms before giving up will make failure to save rare enough to be ok. --- .../org/eehouse/android/xw4/GameConfigDelegate.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java index 235d18e5a..33be78a13 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameConfigDelegate.java @@ -1246,19 +1246,12 @@ public class GameConfigDelegate extends DelegateBase if ( null != m_jniThread ) { applyChanges( m_jniThread.getLock(), forceNew ); } else { - try ( GameLock lock = GameLock.tryLock( m_rowid ) ) { + try ( GameLock lock = GameLock.lock( m_rowid, 100L ) ) { applyChanges( lock, forceNew ); + } catch ( GameLock.GameLockedException gle ) { + Log.e( TAG, "applyChanges(): failed to get lock" ); } } - // } - // GameLock gameLock = m_jniThread == null - // ? GameLock.tryLock( m_rowid ) : m_jniThread.getLock(); - // GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap, - // gameLock, forceNew ); - // DBUtils.saveThumbnail( m_activity, gameLock, null ); // clear it - // if ( null == m_jniThread ) { - // gameLock.unlock(); - // } } } From 97a80e70849ee542fa6c2898c0d68b02d5d0727b Mon Sep 17 00:00:00 2001 From: Eric House Date: Fri, 20 Dec 2019 20:34:42 -0800 Subject: [PATCH 09/24] clean up email invite string The tap-here link and attachment weren't working, so remove them. --- xwords4/android/app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 0b55fb325..f2a7aafd0 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -1039,7 +1039,7 @@ substituted for "%1$s". (The funky \u003c and friends are encodings for the greater-than and less-than symbols which are not legal in xml strings.)--> - <a href=\"%1$s\">Tap here</a> (or the full link below to join this game. If you already have CrossWords you can open the attachment to do so). <br \\> <br \\> (Full link: %1$s ) + Tap the full link below to join this game. <br \\> <br \\> (Full link: %1$s ) Tap the link to accept my invitation and join a CrossWords game: %1$s From ccaa3c67fc9c2f943b831fea906e7aea8541fe46 Mon Sep 17 00:00:00 2001 From: Eric House Date: Sat, 30 Nov 2019 22:43:30 -0800 Subject: [PATCH 10/24] use enums for table names Cleans up the code a bit, adding type-safety where it probably wasn't needed but also letting me iterate over all tables. Not being able to do the latter had allowed db-replacement code (useful only for debugging, probably) to get out of date. --- .../org/eehouse/android/xw4/DBHelper.java | 138 +++++---- .../java/org/eehouse/android/xw4/DBUtils.java | 275 ++++++++---------- 2 files changed, 199 insertions(+), 214 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java index f9e1128d8..d13738faa 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBHelper.java @@ -35,17 +35,26 @@ import java.util.Arrays; public class DBHelper extends SQLiteOpenHelper { private static final String TAG = DBHelper.class.getSimpleName(); - public static final String TABLE_NAME_SUM = "summaries"; - public static final String TABLE_NAME_OBITS = "obits"; - public static final String TABLE_NAME_DICTBROWSE = "dictbrowse"; - public static final String TABLE_NAME_DICTINFO = "dictinfo"; - public static final String TABLE_NAME_GROUPS = "groups"; - public static final String TABLE_NAME_STUDYLIST = "study"; - public static final String TABLE_NAME_LOC = "loc"; - public static final String TABLE_NAME_PAIRS = "pairs"; - public static final String TABLE_NAME_INVITES = "invites"; - public static final String TABLE_NAME_CHAT = "chat"; - public static final String TABLE_NAME_LOGS = "logs"; + public enum TABLE_NAMES { + SUM( "summaries", 0 ), + OBITS( "obits", 5 ), + DICTBROWSE( "dictbrowse", 12 ), + DICTINFO( "dictinfo", 12 ), + GROUPS( "groups", 14 ), + STUDYLIST( "study", 18 ), + LOC( "loc", 20 ), + PAIRS( "pairs", 21 ), + INVITES( "invites", 24 ), + CHAT( "chat", 25 ), + LOGS( "logs", 26 ); + + private String mName; + private int mAddedVersion; + private TABLE_NAMES(String name, int start) { mName = name; mAddedVersion = start; } + @Override + public String toString() { return mName; } + private int addedVersion() { return mAddedVersion; } + } private static final String DB_NAME = BuildConfig.DB_NAME; private static final int DB_VERSION = 29; @@ -245,11 +254,11 @@ public class DBHelper extends SQLiteOpenHelper { @Override public void onCreate( SQLiteDatabase db ) { - createTable( db, TABLE_NAME_SUM, s_summaryColsAndTypes ); - createTable( db, TABLE_NAME_OBITS, s_obitsColsAndTypes ); - createTable( db, TABLE_NAME_DICTINFO, s_dictInfoColsAndTypes ); - createTable( db, TABLE_NAME_DICTBROWSE, s_dictBrowseColsAndTypes ); - forceRowidHigh( db, TABLE_NAME_SUM ); + createTable( db, TABLE_NAMES.SUM, s_summaryColsAndTypes ); + createTable( db, TABLE_NAMES.OBITS, s_obitsColsAndTypes ); + createTable( db, TABLE_NAMES.DICTINFO, s_dictInfoColsAndTypes ); + createTable( db, TABLE_NAMES.DICTBROWSE, s_dictBrowseColsAndTypes ); + forceRowidHigh( db, TABLE_NAMES.SUM ); createGroupsTable( db, false ); createStudyTable( db ); createLocTable( db ); @@ -269,7 +278,7 @@ public class DBHelper extends SQLiteOpenHelper { boolean madeChatTable = false; switch( oldVersion ) { case 5: - createTable( db, TABLE_NAME_OBITS, s_obitsColsAndTypes ); + createTable( db, TABLE_NAMES.OBITS, s_obitsColsAndTypes ); case 6: addSumColumn( db, TURN ); addSumColumn( db, GIFLAGS ); @@ -285,8 +294,8 @@ public class DBHelper extends SQLiteOpenHelper { case 11: addSumColumn( db, REMOTEDEVS ); case 12: - createTable( db, TABLE_NAME_DICTINFO, s_dictInfoColsAndTypes ); - createTable( db, TABLE_NAME_DICTBROWSE, s_dictBrowseColsAndTypes ); + createTable( db, TABLE_NAMES.DICTINFO, s_dictInfoColsAndTypes ); + createTable( db, TABLE_NAMES.DICTBROWSE, s_dictBrowseColsAndTypes ); case 13: addSumColumn( db, LASTMOVE ); case 14: @@ -296,8 +305,8 @@ public class DBHelper extends SQLiteOpenHelper { moveToCurGames( db ); case 16: addSumColumn( db, VISID ); - setColumnsEqual( db, TABLE_NAME_SUM, VISID, "rowid" ); - makeAutoincrement( db, TABLE_NAME_SUM, s_summaryColsAndTypes ); + setColumnsEqual( db, TABLE_NAMES.SUM, VISID, "rowid" ); + makeAutoincrement( db, TABLE_NAMES.SUM, s_summaryColsAndTypes ); madeSumTable = true; case 17: if ( !madeSumTable ) { @@ -336,25 +345,14 @@ public class DBHelper extends SQLiteOpenHelper { } case 28: if ( !madeChatTable ) { - addColumn( db, TABLE_NAME_CHAT, s_chatsSchema, CHATTIME ); + addColumn( db, TABLE_NAMES.CHAT, s_chatsSchema, CHATTIME ); } break; default: - db.execSQL( "DROP TABLE " + TABLE_NAME_SUM + ";" ); - - TableAndVersion[] tav = new TableAndVersion[] { - new TableAndVersion( TABLE_NAME_OBITS, 5 ), - new TableAndVersion( TABLE_NAME_DICTINFO, 12 ), - new TableAndVersion( TABLE_NAME_DICTBROWSE, 12 ), - new TableAndVersion( TABLE_NAME_GROUPS, 14 ), - new TableAndVersion( TABLE_NAME_STUDYLIST, 18 ), - new TableAndVersion( TABLE_NAME_LOC, 20 ), - new TableAndVersion( TABLE_NAME_PAIRS, 21 ), - }; - for ( TableAndVersion entry : tav ) { - if ( oldVersion >= 1 + entry.addedVersion ) { - db.execSQL( "DROP TABLE " + entry.name + ";" ); + for ( TABLE_NAMES table : TABLE_NAMES.values() ) { + if ( oldVersion >= 1 + table.addedVersion() ) { + db.execSQL( "DROP TABLE " + table + ";" ); } } onCreate( db ); @@ -363,10 +361,10 @@ public class DBHelper extends SQLiteOpenHelper { private void addSumColumn( SQLiteDatabase db, String colName ) { - addColumn( db, TABLE_NAME_SUM, s_summaryColsAndTypes, colName ); + addColumn( db, TABLE_NAMES.SUM, s_summaryColsAndTypes, colName ); } - private void addColumn( SQLiteDatabase db, String tableName, + private void addColumn( SQLiteDatabase db, TABLE_NAMES tableName, String[][] colsAndTypes, String colName ) { String colType = null; @@ -382,7 +380,7 @@ public class DBHelper extends SQLiteOpenHelper { db.execSQL( cmd ); } - private void createTable( SQLiteDatabase db, String name, String[][] data ) + private void createTable( SQLiteDatabase db, TABLE_NAMES name, String[][] data ) { StringBuilder query = new StringBuilder( String.format("CREATE TABLE %s (", name ) ); @@ -404,7 +402,7 @@ public class DBHelper extends SQLiteOpenHelper { isUpgrade = 0 < countGames( db ); } - createTable( db, TABLE_NAME_GROUPS, s_groupsSchema ); + createTable( db, TABLE_NAMES.GROUPS, s_groupsSchema ); // Create an empty group name ContentValues values = new ContentValues(); @@ -412,50 +410,50 @@ public class DBHelper extends SQLiteOpenHelper { values.put( GROUPNAME, LocUtils.getString( m_context, false, R.string.group_cur_games) ); values.put( EXPANDED, 1 ); - long curGroup = db.insert( TABLE_NAME_GROUPS, null, values ); + long curGroup = insert( db, TABLE_NAMES.GROUPS, values ); // place all existing games in the initial unnamed group values = new ContentValues(); values.put( GROUPID, curGroup ); - db.update( DBHelper.TABLE_NAME_SUM, values, null, null ); + db.update( DBHelper.TABLE_NAMES.SUM.toString(), values, null, null ); } values = new ContentValues(); values.put( GROUPNAME, LocUtils.getString( m_context, false, R.string.group_new_games) ); values.put( EXPANDED, 1 ); - long newGroup = db.insert( TABLE_NAME_GROUPS, null, values ); + long newGroup = insert( db, TABLE_NAMES.GROUPS, values ); XWPrefs.setDefaultNewGameGroup( m_context, newGroup ); } private void createStudyTable( SQLiteDatabase db ) { - createTable( db, TABLE_NAME_STUDYLIST, s_studySchema ); + createTable( db, TABLE_NAMES.STUDYLIST, s_studySchema ); } private void createLocTable( SQLiteDatabase db ) { - createTable( db, TABLE_NAME_LOC, s_locSchema ); + createTable( db, TABLE_NAMES.LOC, s_locSchema ); } private void createPairsTable( SQLiteDatabase db ) { - createTable( db, TABLE_NAME_PAIRS, s_pairsSchema ); + createTable( db, TABLE_NAMES.PAIRS, s_pairsSchema ); } private void createInvitesTable( SQLiteDatabase db ) { - createTable( db, TABLE_NAME_INVITES, s_invitesSchema ); + createTable( db, TABLE_NAMES.INVITES, s_invitesSchema ); } private void createChatsTable( SQLiteDatabase db ) { - createTable( db, TABLE_NAME_CHAT, s_chatsSchema ); + createTable( db, TABLE_NAMES.CHAT, s_chatsSchema ); } private void createLogsTable( SQLiteDatabase db ) { - createTable( db, TABLE_NAME_LOGS, s_logsSchema ); + createTable( db, TABLE_NAMES.LOGS, s_logsSchema ); } // Move all existing games to the row previously named "cur games' @@ -465,19 +463,18 @@ public class DBHelper extends SQLiteOpenHelper { R.string.group_cur_games ); String[] columns = { "rowid" }; String selection = String.format( "%s = '%s'", GROUPNAME, name ); - Cursor cursor = db.query( DBHelper.TABLE_NAME_GROUPS, columns, - selection, null, null, null, null ); + Cursor cursor = query( db, TABLE_NAMES.GROUPS, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { long rowid = cursor.getLong( cursor.getColumnIndex("rowid") ); ContentValues values = new ContentValues(); values.put( GROUPID, rowid ); - db.update( DBHelper.TABLE_NAME_SUM, values, null, null ); + update( db, TABLE_NAMES.SUM, values, null ); } cursor.close(); } - private void makeAutoincrement( SQLiteDatabase db, String name, + private void makeAutoincrement( SQLiteDatabase db, TABLE_NAMES name, String[][] data ) { db.beginTransaction(); @@ -524,7 +521,7 @@ public class DBHelper extends SQLiteOpenHelper { } } - private void setColumnsEqual( SQLiteDatabase db, String table, + private void setColumnsEqual( SQLiteDatabase db, TABLE_NAMES table, String dest, String src ) { String query = String.format( "UPDATE %s set %s = %s", table, @@ -532,7 +529,7 @@ public class DBHelper extends SQLiteOpenHelper { db.execSQL( query ); } - private void forceRowidHigh( SQLiteDatabase db, String name ) + private void forceRowidHigh( SQLiteDatabase db, TABLE_NAMES name ) { long now = Utils.getCurSeconds(); // knock 20 years off; whose clock can be that far back? @@ -546,7 +543,7 @@ public class DBHelper extends SQLiteOpenHelper { private int countGames( SQLiteDatabase db ) { - final String query = "SELECT COUNT(*) FROM " + TABLE_NAME_SUM; + final String query = "SELECT COUNT(*) FROM " + TABLE_NAMES.SUM; Cursor cursor = db.rawQuery( query, null ); cursor.moveToFirst(); @@ -555,7 +552,7 @@ public class DBHelper extends SQLiteOpenHelper { return result; } - private static String[] getColumns( SQLiteDatabase db, String name ) + private static String[] getColumns( SQLiteDatabase db, TABLE_NAMES name ) { String query = String.format( "SELECT * FROM %s LIMIT 1", name ); Cursor cursor = db.rawQuery( query, null ); @@ -564,12 +561,27 @@ public class DBHelper extends SQLiteOpenHelper { return colNames; } - private class TableAndVersion { - public String name; - public int addedVersion; - public TableAndVersion( String nn, int vers ) { - name = nn; addedVersion = vers; - } + static Cursor query( SQLiteDatabase db, TABLE_NAMES table, String[] columns, + String selection, String orderBy ) + { + return db.query( table.toString(), columns, selection, + null, null, null, orderBy ); } + static Cursor query( SQLiteDatabase db, TABLE_NAMES table, String[] columns, + String selection ) + { + return query( db, table, columns, selection, null ); + } + + public static int update( SQLiteDatabase db, TABLE_NAMES table, ContentValues values, + String selection ) + { + return db.update( table.toString(), values, selection, null ); + } + + static long insert( SQLiteDatabase db, TABLE_NAMES table, ContentValues values ) + { + return db.insert( table.toString(), null, values ); + } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java index 5997f73a5..65f6a5e18 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java @@ -33,7 +33,7 @@ import android.graphics.BitmapFactory; import android.os.Environment; import android.text.TextUtils; - +import org.eehouse.android.xw4.DBHelper.TABLE_NAMES; import org.eehouse.android.xw4.DictUtils.DictLoc; import org.eehouse.android.xw4.DlgDelegate.DlgClickNotify.InviteMeans; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; @@ -151,8 +151,7 @@ public class DBUtils { String selection = String.format( ROW_ID_FMT, lock.getRowid() ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { summary = new GameSummary(); summary.nMoves = cursor @@ -350,10 +349,9 @@ public class DBUtils { synchronized( s_dbHelper ) { if ( null == summary ) { - s_db.delete( DBHelper.TABLE_NAME_SUM, selection, null ); + delete( TABLE_NAMES.SUM, selection ); } else { - long result = s_db.update( DBHelper.TABLE_NAME_SUM, - values, selection, null ); + long result = update( TABLE_NAMES.SUM, values, selection ); Assert.assertTrue( result >= 0 ); } notifyListeners( rowid, GameChangeType.GAME_CHANGED ); @@ -404,8 +402,7 @@ public class DBUtils { String[] columns = { DBHelper.DICTLANG }; initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); result = cursor.getCount(); cursor.close(); @@ -425,8 +422,7 @@ public class DBUtils { String[] columns = { DBHelper.DICTLANG }; initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); result = cursor.getCount(); cursor.close(); } @@ -442,8 +438,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); int indx = cursor.getColumnIndex( DBHelper.CONTYPE ); while ( cursor.moveToNext() ) { CommsConnTypeSet typs = new CommsConnTypeSet( cursor.getInt(indx) ); @@ -622,8 +617,7 @@ public class DBUtils { String orderBy = DBHelper.TIMESTAMP + " DESC"; synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_INVITES, columns, - selection, null, null, null, orderBy ); + Cursor cursor = DBHelper.query( s_db, TABLE_NAMES.INVITES, columns, selection, orderBy ); if ( 0 < cursor.getCount() ) { int indxMns = cursor.getColumnIndex( DBHelper.MEANS ); int indxTS = cursor.getColumnIndex( DBHelper.TIMESTAMP ); @@ -654,7 +648,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - s_db.insert( DBHelper.TABLE_NAME_INVITES, null, values ); + insert( TABLE_NAMES.INVITES, values ); } } @@ -663,7 +657,7 @@ public class DBUtils { { ContentValues values = new ContentValues(); values.put( column, value ); - updateRow( null, DBHelper.TABLE_NAME_SUM, rowid, values ); + updateRow( null, TABLE_NAMES.SUM, rowid, values ); } public static void setMsgFlags( long rowid, int flags ) @@ -685,8 +679,7 @@ public class DBUtils { String[] columns = { column }; initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { result = cursor.getInt( cursor.getColumnIndex(column)); @@ -730,8 +723,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - long result = s_db.update( DBHelper.TABLE_NAME_SUM, - values, selection, null ); + long result = update( TABLE_NAMES.SUM, values, selection ); Assert.assertTrue( result >= 0 ); @@ -745,8 +737,7 @@ public class DBUtils { values.putNull( DBHelper.THUMBNAIL ); initDB( context ); synchronized( s_dbHelper ) { - long result = s_db.update( DBHelper.TABLE_NAME_SUM, - values, null, null ); + long result = update( TABLE_NAMES.SUM, values, null ); notifyListeners( ROWIDS_ALL, GameChangeType.GAME_CHANGED ); } @@ -759,8 +750,7 @@ public class DBUtils { String selection = String.format( ROW_ID_FMT, rowid ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { result = cursor.getString( cursor.getColumnIndex(DBHelper.RELAYID) ); @@ -779,8 +769,7 @@ public class DBUtils { DBHelper.GROUPID, getArchiveGroup( context ) ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); int indx1 = cursor.getColumnIndex( ROW_ID ); int indx2 = cursor.getColumnIndex( DBHelper.CONTYPE ); for ( int ii = 0; cursor.moveToNext(); ++ii ) { @@ -802,8 +791,7 @@ public class DBUtils { String selection = String.format( "%s = 0", DBHelper.GAME_OVER ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); int indx = cursor.getColumnIndex( DBHelper.CONTYPE ); while ( cursor.moveToNext() ) { CommsConnTypeSet typs = new CommsConnTypeSet( cursor.getInt(indx) ); @@ -823,8 +811,7 @@ public class DBUtils { String selection = DBHelper.RELAYID + "='" + relayID + "'"; initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); result = new long[cursor.getCount()]; for ( int ii = 0; cursor.moveToNext(); ++ii ) { result[ii] = cursor.getLong( cursor.getColumnIndex(ROW_ID) ); @@ -841,8 +828,7 @@ public class DBUtils { String selection = String.format( DBHelper.GAMEID + "=%d", gameID ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); result = new long[cursor.getCount()]; for ( int ii = 0; cursor.moveToNext(); ++ii ) { result[ii] = cursor.getLong( cursor.getColumnIndex(ROW_ID) ); @@ -869,8 +855,7 @@ public class DBUtils { String selection = String.format( ROW_ID + "=%d", rowid ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); Assert.assertTrue( 1 >= cursor.getCount() ); result = 1 == cursor.getCount(); cursor.close(); @@ -888,8 +873,7 @@ public class DBUtils { new HashMap >(); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); while ( cursor.moveToNext() ) { int col = cursor.getColumnIndex( DBHelper.GAMEID ); int gameID = cursor.getInt( col ); @@ -936,9 +920,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( cursor.moveToNext() ) { int indx = cursor.getColumnIndex( columns[0] ); result = new Date( cursor.getLong( indx ) ); @@ -958,9 +940,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); int count = cursor.getCount(); if ( 0 < count ) { result = new String[count]; @@ -1002,7 +982,7 @@ public class DBUtils { synchronized( s_dbHelper ) { try { - long result = s_db.replaceOrThrow( DBHelper.TABLE_NAME_OBITS, + long result = s_db.replaceOrThrow( TABLE_NAMES.OBITS.toString(), "", values ); } catch ( Exception ex ) { Log.ex( TAG, ex ); @@ -1018,8 +998,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_OBITS, columns, - null, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.OBITS, columns, null ); if ( 0 < cursor.getCount() ) { int idIndex = cursor.getColumnIndex( DBHelper.RELAYID ); int seedIndex = cursor.getColumnIndex( DBHelper.SEED ); @@ -1050,7 +1029,7 @@ public class DBUtils { for ( Obit obit: obits ) { String selection = String.format( fmt, obit.m_relayID, obit.m_seed ); - s_db.delete( DBHelper.TABLE_NAME_OBITS, selection, null ); + delete( TABLE_NAMES.OBITS, selection ); } } } @@ -1078,7 +1057,7 @@ public class DBUtils { synchronized( s_dbHelper ) { values.put( DBHelper.VISID, maxVISID( s_db ) ); - long rowid = s_db.insert( DBHelper.TABLE_NAME_SUM, null, values ); + long rowid = insert( TABLE_NAMES.SUM, values ); setCached( rowid, null ); // force reread @@ -1106,7 +1085,7 @@ public class DBUtils { } values.put( DBHelper.LASTPLAY_TIME, timestamp ); - updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values ); + updateRow( context, TABLE_NAMES.SUM, rowid, values ); setCached( rowid, null ); // force reread if ( ROWID_NOTFOUND != rowid ) { // Means new game? @@ -1126,9 +1105,7 @@ public class DBUtils { String selection = String.format( ROW_ID_FMT, rowid ); initDB( context ); synchronized( s_dbHelper ) { - - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { result = cursor.getBlob( cursor .getColumnIndex(DBHelper.SNAPSHOT)); @@ -1163,13 +1140,13 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - s_db.delete( DBHelper.TABLE_NAME_SUM, selSummaries, null ); + delete( TABLE_NAMES.SUM, selSummaries ); // Delete invitations too - s_db.delete( DBHelper.TABLE_NAME_INVITES, selInvites, null ); + delete( TABLE_NAMES.INVITES, selInvites ); // Delete chats too -- same sel as for invites - s_db.delete( DBHelper.TABLE_NAME_CHAT, selInvites, null ); + delete( TABLE_NAMES.CHAT, selInvites ); deleteCurChatsSync( s_db, rowid ); @@ -1185,9 +1162,7 @@ public class DBUtils { String selection = String.format( ROW_ID_FMT, rowid ); initDB( context ); synchronized( s_dbHelper ) { - - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { result = cursor.getInt( cursor .getColumnIndex(DBHelper.VISID)); @@ -1206,9 +1181,7 @@ public class DBUtils { String selection = String.format( ROW_ID_FMT, rowid ); initDB( context ); synchronized( s_dbHelper ) { - - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { result = cursor.getString( cursor .getColumnIndex(DBHelper.GAME_NAME)); @@ -1223,7 +1196,7 @@ public class DBUtils { { ContentValues values = new ContentValues(); values.put( DBHelper.GAME_NAME, name ); - updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values ); + updateRow( context, TABLE_NAMES.SUM, rowid, values ); } private static HistoryPair[] convertChatString( Context context, long rowid, @@ -1291,8 +1264,7 @@ public class DBUtils { String selection = String.format( "%s=%d", DBHelper.ROW, rowid ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_CHAT, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.CHAT, columns, selection ); if ( 0 < cursor.getCount() ) { result = new HistoryPair[cursor.getCount()]; int msgIndex = cursor.getColumnIndex( DBHelper.MESSAGE ); @@ -1386,8 +1358,7 @@ public class DBUtils { DBHelper.NEXTNAG, now ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); int count = cursor.getCount(); if ( 0 < count ) { result = new NeedsNagInfo[count]; @@ -1417,8 +1388,7 @@ public class DBUtils { String selection = "NOT " + DBHelper.NEXTNAG + "= 0"; initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( cursor.moveToNext() ) { result = cursor.getLong( cursor.getColumnIndex( "min" ) ); } @@ -1431,7 +1401,7 @@ public class DBUtils { { String updateQuery = "update %s set %s = ? " + " WHERE %s = ? "; - updateQuery = String.format( updateQuery, DBHelper.TABLE_NAME_SUM, + updateQuery = String.format( updateQuery, DBHelper.TABLE_NAMES.SUM, DBHelper.NEXTNAG, ROW_ID ); initDB( context ); @@ -1484,8 +1454,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { data = cursor.getBlob( cursor. getColumnIndex(DBHelper.THUMBNAIL)); @@ -1504,7 +1473,7 @@ public class DBUtils { HashMap result = new HashMap(); String query = "SELECT %s, count(%s) as cnt FROM %s GROUP BY %s"; query = String.format( query, DBHelper.GROUPID, DBHelper.GROUPID, - DBHelper.TABLE_NAME_SUM, DBHelper.GROUPID ); + DBHelper.TABLE_NAMES.SUM, DBHelper.GROUPID ); Cursor cursor = db.rawQuery( query, null ); int rowIndex = cursor.getColumnIndex( DBHelper.GROUPID ); @@ -1573,13 +1542,8 @@ public class DBUtils { DBHelper.TURN }; String orderBy = DBHelper.LASTMOVE; String selection = String.format( "%s=%d", DBHelper.GROUPID, groupID ); - Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, - null, // args - null, // groupBy, - null, // having - orderBy - ); + Cursor cursor = DBHelper.query( db, TABLE_NAMES.SUM, columns, + selection, orderBy ); // We want the earliest LASTPLAY_TIME (i.e. the first we see // since they're in order) that's a local turn, if any, @@ -1625,13 +1589,7 @@ public class DBUtils { String[] columns = { ROW_ID }; initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - null, // selection - null, // args - null, // groupBy - null, // having - null - ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, null ); result = cursor.getCount(); cursor.close(); } @@ -1660,7 +1618,7 @@ public class DBUtils { String[] columns = { ROW_ID, DBHelper.HASMSGS }; String selection = String.format( "%s=%d", DBHelper.GROUPID, groupID ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, + Cursor cursor = s_db.query( TABLE_NAMES.SUM.toString(), columns, selection, // selection null, // args null, // groupBy @@ -1695,13 +1653,7 @@ public class DBUtils { } synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, // selection - null, // args - null, // groupBy - null, // having - null //orderby - ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( cursor.moveToNext() ) { int index = cursor.getColumnIndex( DBHelper.GROUPID ); result = cursor.getLong( index ); @@ -1732,7 +1684,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_GROUPS, columns, + Cursor cursor = s_db.query( TABLE_NAMES.GROUPS.toString(), columns, selection, selArgs, null, // groupBy null, // having @@ -1758,8 +1710,7 @@ public class DBUtils { // initDB( context ); <- getGroups will have called this synchronized( s_dbHelper ) { - rowid = s_db.insert( DBHelper.TABLE_NAME_GROUPS, null, - values ); + rowid = insert( TABLE_NAMES.GROUPS, values ); } invalGroupsCache(); } @@ -1778,9 +1729,8 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - s_db.delete( DBHelper.TABLE_NAME_SUM, selectionGames, null ); - s_db.delete( DBHelper.TABLE_NAME_GROUPS, selectionGroups, null ); - + delete( TABLE_NAMES.SUM, selectionGames ); + delete( TABLE_NAMES.GROUPS, selectionGroups ); } invalGroupsCache(); } @@ -1790,7 +1740,7 @@ public class DBUtils { { ContentValues values = new ContentValues(); values.put( DBHelper.GROUPNAME, name ); - updateRow( context, DBHelper.TABLE_NAME_GROUPS, groupid, values ); + updateRow( context, TABLE_NAMES.GROUPS, groupid, values ); invalGroupsCache(); } @@ -1799,7 +1749,7 @@ public class DBUtils { { ContentValues values = new ContentValues(); values.put( DBHelper.EXPANDED, expanded? 1 : 0 ); - updateRow( context, DBHelper.TABLE_NAME_GROUPS, groupid, values ); + updateRow( context, TABLE_NAMES.GROUPS, groupid, values ); invalGroupsCache(); } @@ -1820,7 +1770,7 @@ public class DBUtils { Assert.assertTrue( GROUPID_UNSPEC != groupID ); ContentValues values = new ContentValues(); values.put( DBHelper.GROUPID, groupID ); - updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values ); + updateRow( context, TABLE_NAMES.SUM, rowid, values ); invalGroupsCache(); notifyListeners( rowid, GameChangeType.GAME_MOVED ); } @@ -1832,9 +1782,7 @@ public class DBUtils { String selection = String.format( ROW_ID_FMT, rowid ); initDB( context ); synchronized( s_dbHelper ) { - - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_SUM, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.SUM, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { result = cursor.getString( cursor @@ -1852,7 +1800,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { for ( ContentValues values : valuess ) { - s_db.insert( DBHelper.TABLE_NAME_CHAT, null, values ); + insert( TABLE_NAMES.CHAT, values ); } } } @@ -1885,15 +1833,14 @@ public class DBUtils { String selection = String.format( "%s = %d", DBHelper.ROW, rowid ); initDB( context ); synchronized( s_dbHelper ) { - s_db.delete( DBHelper.TABLE_NAME_CHAT, selection, null ); + delete( TABLE_NAMES.CHAT, selection ); // for now, remove any old-format history too. Later when it's // removed once converted (after that process is completely // debugged), this can be removed. ContentValues values = new ContentValues(); values.putNull( DBHelper.CHAT_HISTORY ); - updateRowImpl( s_db, DBHelper.TABLE_NAME_SUM, rowid, values ); - + updateRowImpl( s_db, TABLE_NAMES.SUM, rowid, values ); } } @@ -1977,8 +1924,7 @@ public class DBUtils { name, DBHelper.LOC, loc.ordinal() ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_DICTBROWSE, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.DICTBROWSE, columns, selection ); if ( 1 >= cursor.getCount() && cursor.moveToFirst() ) { result = new DictBrowseState(); result.m_pos = cursor.getInt( cursor @@ -2033,12 +1979,11 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - int result = s_db.update( DBHelper.TABLE_NAME_DICTBROWSE, - values, selection, null ); + int result = update( TABLE_NAMES.DICTBROWSE, values, selection ); if ( 0 == result ) { values.put( DBHelper.DICTNAME, name ); values.put( DBHelper.LOC, loc.ordinal() ); - s_db.insert( DBHelper.TABLE_NAME_DICTBROWSE, null, values ); + insert( TABLE_NAMES.DICTBROWSE, values ); } } } @@ -2059,11 +2004,10 @@ public class DBUtils { values.put( DBHelper.MD5SUM, sum ); initDB( context ); synchronized( s_dbHelper ) { - int result = s_db.update( DBHelper.TABLE_NAME_DICTINFO, - values, selection, null ); + int result = update( TABLE_NAMES.DICTINFO, values, selection ); if ( 0 == result ) { values.put( DBHelper.DICTNAME, name ); - long rowid = s_db.insert( DBHelper.TABLE_NAME_DICTINFO, null, values ); + long rowid = insert( TABLE_NAMES.DICTINFO, values ); Assert.assertTrue( rowid > 0 || !BuildConfig.DEBUG ); } } @@ -2079,8 +2023,7 @@ public class DBUtils { String selection = String.format( NAME_FMT, DBHelper.DICTNAME, name ); initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_DICTINFO, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.DICTINFO, columns, selection ); if ( 1 == cursor.getCount() && cursor.moveToFirst() ) { result = new DictInfo(); result.name = name; @@ -2113,12 +2056,10 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - int result = s_db.update( DBHelper.TABLE_NAME_DICTINFO, - values, selection, null ); + int result = update( TABLE_NAMES.DICTINFO, values, selection ); if ( 0 == result ) { values.put( DBHelper.DICTNAME, dal.name ); - long rowid = s_db.insert( DBHelper.TABLE_NAME_DICTINFO, null, - values ); + long rowid = insert( TABLE_NAMES.DICTINFO, values ); Assert.assertTrue( rowid > 0 || !BuildConfig.DEBUG ); } } @@ -2135,8 +2076,8 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - s_db.update( DBHelper.TABLE_NAME_DICTINFO, values, selection, null ); - s_db.update( DBHelper.TABLE_NAME_DICTBROWSE, values, selection, null ); + update( TABLE_NAMES.DICTINFO, values, selection ); + update( TABLE_NAMES.DICTBROWSE, values, selection ); } } @@ -2147,9 +2088,9 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - int removed = s_db.delete( DBHelper.TABLE_NAME_DICTINFO, selection, args ); + int removed = delete( TABLE_NAMES.DICTINFO, selection, args ); // Log.d( TAG, "removed %d rows from %s", removed, DBHelper.TABLE_NAME_DICTINFO ); - removed = s_db.delete( DBHelper.TABLE_NAME_DICTBROWSE, selection, args ); + removed = delete( TABLE_NAMES.DICTBROWSE, selection, args ); // Log.d( TAG, "removed %d rows from %s", removed, DBHelper.TABLE_NAME_DICTBROWSE ); } } @@ -2176,7 +2117,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - s_db.insert( DBHelper.TABLE_NAME_STUDYLIST, null, values ); + insert( TABLE_NAMES.STUDYLIST, values ); } notifyStudyListListeners( word, lang ); } @@ -2190,7 +2131,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_STUDYLIST, columns, + Cursor cursor = s_db.query( TABLE_NAMES.STUDYLIST.toString(), columns, null, null, groupBy, null, null ); int count = cursor.getCount(); result = new int[count]; @@ -2215,8 +2156,8 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_STUDYLIST, columns, - selection, null, null, null, orderBy ); + Cursor cursor = DBHelper.query( s_db, TABLE_NAMES.STUDYLIST, columns, + selection, orderBy ); int count = cursor.getCount(); result = new String[count]; if ( 0 < count ) { @@ -2241,7 +2182,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - s_db.delete( DBHelper.TABLE_NAME_STUDYLIST, selection, null ); + delete( TABLE_NAMES.STUDYLIST, selection ); } } @@ -2259,13 +2200,13 @@ public class DBUtils { String insertQuery = "insert into %s (%s, %s, %s, %s) " + " VALUES (?, ?, ?, ?)"; - insertQuery = String.format( insertQuery, DBHelper.TABLE_NAME_LOC, + insertQuery = String.format( insertQuery, TABLE_NAMES.LOC, DBHelper.KEY, DBHelper.LOCALE, DBHelper.BLESSED, DBHelper.XLATION ); String updateQuery = "update %s set %s = ? " + " WHERE %s = ? and %s = ? and %s = ?"; - updateQuery = String.format( updateQuery, DBHelper.TABLE_NAME_LOC, + updateQuery = String.format( updateQuery, TABLE_NAMES.LOC, DBHelper.XLATION, DBHelper.KEY, DBHelper.LOCALE, DBHelper.BLESSED ); @@ -2319,8 +2260,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - Cursor cursor = s_db.query( DBHelper.TABLE_NAME_LOC, columns, - selection, null, null, null, null ); + Cursor cursor = query( TABLE_NAMES.LOC, columns, selection ); int keyIndex = cursor.getColumnIndex( DBHelper.KEY ); int valueIndex = cursor.getColumnIndex( DBHelper.XLATION ); int blessedIndex = cursor.getColumnIndex( DBHelper.BLESSED ); @@ -2345,7 +2285,7 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - s_db.delete( DBHelper.TABLE_NAME_LOC, selection, null ); + delete( TABLE_NAMES.LOC, selection ); } } @@ -2355,18 +2295,17 @@ public class DBUtils { ContentValues values = new ContentValues(); values.put( DBHelper.VALUE, value ); - long result = db.update( DBHelper.TABLE_NAME_PAIRS, - values, selection, null ); + long result = DBHelper.update( db, TABLE_NAMES.PAIRS, values, selection ); if ( 0 == result ) { values.put( DBHelper.KEY, key ); - db.insert( DBHelper.TABLE_NAME_PAIRS, null, values ); + DBHelper.insert( db, TABLE_NAMES.PAIRS, values ); } } private static void delStringsLikeSync( SQLiteDatabase db, String like ) { String selection = String.format( "%s LIKE '%s'", DBHelper.KEY, like ); - db.delete( DBHelper.TABLE_NAME_PAIRS, selection, null ); + delete( db, TABLE_NAMES.PAIRS, selection, null ); } private static String getStringForSync( SQLiteDatabase db, String key, String dflt ) @@ -2374,8 +2313,7 @@ public class DBUtils { String selection = String.format( "%s = '%s'", DBHelper.KEY, key ); String[] columns = { DBHelper.VALUE }; - Cursor cursor = db.query( DBHelper.TABLE_NAME_PAIRS, columns, - selection, null, null, null, null ); + Cursor cursor = DBHelper.query( db, TABLE_NAMES.PAIRS, columns, selection ); Assert.assertTrue( 1 >= cursor.getCount() ); int indx = cursor.getColumnIndex( DBHelper.VALUE ); if ( cursor.moveToNext() ) { @@ -2536,13 +2474,13 @@ public class DBUtils { initDB( context ); synchronized( s_dbHelper ) { - long rowid = s_db.insert( DBHelper.TABLE_NAME_LOGS, null, values ); + long rowid = insert( TABLE_NAMES.LOGS, values ); if ( 0 == (rowid % (LOGLIMIT / 10)) ) { String where = String.format( "not rowid in (select rowid from %s order by TIMESTAMP desc limit %d)", - DBHelper.TABLE_NAME_LOGS, LOGLIMIT ); - int nGone = s_db.delete( DBHelper.TABLE_NAME_LOGS, where, null ); + TABLE_NAMES.LOGS, LOGLIMIT ); + int nGone = delete( TABLE_NAMES.LOGS, where ); Log.i( TAG, "appendLog(): deleted %d rows", nGone ); } } @@ -2587,7 +2525,7 @@ public class DBUtils { // { // ContentValues values = new ContentValues(); // values.putNull( DBHelper.CHAT_HISTORY ); - // updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values ); + // updateRow( context, DBHelper.TABLE_NAMES.SUM, rowid, values ); // } private static void initDB( Context context ) @@ -2603,15 +2541,15 @@ public class DBUtils { } } - private static int updateRowImpl( SQLiteDatabase db, String table, + private static int updateRowImpl( SQLiteDatabase db, TABLE_NAMES table, long rowid, ContentValues values ) { String selection = String.format( ROW_ID_FMT, rowid ); - return db.update( table, values, selection, null ); + return DBHelper.update( db, table, values, selection ); } - private static void updateRow( Context context, String table, + private static void updateRow( Context context, TABLE_NAMES table, long rowid, ContentValues values ) { initDB( context ); @@ -2627,7 +2565,7 @@ public class DBUtils { { int result = 1; String query = String.format( "SELECT max(%s) FROM %s", DBHelper.VISID, - DBHelper.TABLE_NAME_SUM ); + TABLE_NAMES.SUM ); Cursor cursor = null; try { cursor = db.rawQuery( query, null ); @@ -2676,4 +2614,39 @@ public class DBUtils { s_cachedRowID = rowid; s_cachedBytes = bytes; } + + private static Cursor query( TABLE_NAMES table, String[] columns, String selection ) + { + return DBHelper.query( s_db, table, columns, selection ); + } + + private static int delete( SQLiteDatabase db, TABLE_NAMES table, String selection, String[] args ) + { + return db.delete( table.toString(), selection, args ); + } + + private static int delete( SQLiteDatabase db, TABLE_NAMES table, String selection ) + { + return delete( db, table, selection, null ); + } + + private static int delete( TABLE_NAMES table, String selection ) + { + return delete( s_db, table, selection, null ); + } + + private static int delete( TABLE_NAMES table, String selection, String[] args ) + { + return delete( s_db, table, selection, args ); + } + + private static int update( TABLE_NAMES table, ContentValues values, String selection ) + { + return DBHelper.update( s_db, table, values, selection ); + } + + private static long insert( TABLE_NAMES table, ContentValues values ) + { + return DBHelper.insert( s_db, table, values ); + } } From 0153928bcd54f5c59ec967004131ad1562391f3b Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 23 Dec 2019 08:45:55 -0800 Subject: [PATCH 11/24] support sending invites and moves via NFC Use low-level NFC, a combination of emulated card and reader mode, to work around Google's removal of "beaming" support from Android 10. App emulates a card by declaring support in its AndroidManifest. When a game is open that has data to send, it goes periodically into read mode. If two devices are touched while one is in read mode and the other isn't, they handshake and open a connection that should last until they're separated. The devices loop, sending messages back and forth with or without data (as available.) --- xwords4/android/app/build.gradle | 23 +- .../android/app/src/main/AndroidManifest.xml | 17 +- .../android/app/src/main/assets/changes.html | 2 +- .../org/eehouse/android/xw4/BTService.java | 4 +- .../eehouse/android/xw4/BoardDelegate.java | 107 +-- .../eehouse/android/xw4/CommsTransport.java | 5 +- .../android/xw4/ConnStatusHandler.java | 15 +- .../org/eehouse/android/xw4/DbgUtils.java | 20 +- .../java/org/eehouse/android/xw4/DevID.java | 19 + .../org/eehouse/android/xw4/GameListItem.java | 2 +- .../android/xw4/GamesListDelegate.java | 13 +- .../org/eehouse/android/xw4/MultiMsgSink.java | 9 +- .../org/eehouse/android/xw4/NBSProto.java | 6 +- .../eehouse/android/xw4/NFCCardService.java | 795 ++++++++++++++++++ .../org/eehouse/android/xw4/NFCUtils.java | 455 +++++++--- .../eehouse/android/xw4/NetLaunchInfo.java | 1 + .../org/eehouse/android/xw4/RelayService.java | 6 +- .../java/org/eehouse/android/xw4/Utils.java | 34 + .../org/eehouse/android/xw4/WiDirService.java | 6 +- .../eehouse/android/xw4/jni/CommsAddrRec.java | 2 + .../app/src/main/res/values/common_rsrc.xml | 1 - .../app/src/main/res/values/strings.xml | 4 +- .../app/src/main/res/xml/apduservice.xml | 11 + xwords4/common/comms.c | 17 +- 24 files changed, 1332 insertions(+), 242 deletions(-) create mode 100644 xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java create mode 100644 xwords4/android/app/src/main/res/xml/apduservice.xml diff --git a/xwords4/android/app/build.gradle b/xwords4/android/app/build.gradle index b74c4495a..0e701a83f 100644 --- a/xwords4/android/app/build.gradle +++ b/xwords4/android/app/build.gradle @@ -4,6 +4,10 @@ def VERSION_NAME = '4.4.150' def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY") def BUILD_INFO_NAME = "build-info.txt" +// AID must start with F (first 4 bits) and be at from 5 to 16 bytes long +def NFC_AID_XW4 = "FC8FF510B360" +def NFC_AID_XW4d = "FDDA0A3EB5E5" + boolean forFDroid = hasProperty('forFDroid') // Get the git revision we're using. Since fdroid modifies files as @@ -35,6 +39,7 @@ android { // default changes and .travis.yml can be kept in sync buildToolsVersion '27.0.3' defaultConfig { + // HostApduService requires 19. But is it a problem? minSdkVersion 14 targetSdkVersion 28 // must match ../build.gradle versionCode VERSION_CODE_BASE @@ -88,6 +93,8 @@ android { buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" buildConfigField "String", "VARIANT_NAME", "\"Google Play Store\"" buildConfigField "int", "VARIANT_CODE", "1" + buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\"" + resValue "string", "nfc_aid", "$NFC_AID_XW4" } xw4fdroid { @@ -101,10 +108,12 @@ android { buildConfigField "String", "VARIANT_NAME", "\"F-Droid\"" buildConfigField "int", "VARIANT_CODE", "2" buildConfigField "boolean", "FOR_FDROID", "true" + buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\"" + resValue "string", "nfc_aid", "$NFC_AID_XW4" } xw4d { dimension "variant" - buildConfigField "String", "DB_NAME", "\"xwddb\""; + buildConfigField "String", "DB_NAME", "\"xwddb\"" applicationId "org.eehouse.android.xw4dbg" manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ] resValue "string", "app_name", "CrossDbg" @@ -115,11 +124,13 @@ android { buildConfigField "int", "VARIANT_CODE", "3" buildConfigField "boolean", "REPORT_LOCKS", "true" buildConfigField "boolean", "MOVE_VIA_NFC", "true" + buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\"" + resValue "string", "nfc_aid", "$NFC_AID_XW4d" } xw4dNoSMS { dimension "variant" - buildConfigField "String", "DB_NAME", "\"xwddb\""; + buildConfigField "String", "DB_NAME", "\"xwddb\"" applicationId "org.eehouse.android.xw4dbg" manifestPlaceholders = [ FABRIC_API_KEY: "$FABRIC_API_KEY", APP_ID: applicationId, ] resValue "string", "app_name", "CrossDbg" @@ -130,6 +141,8 @@ android { buildConfigField "int", "VARIANT_CODE", "4" buildConfigField "boolean", "REPORT_LOCKS", "true" buildConfigField "boolean", "MOVE_VIA_NFC", "true" + buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4d}\"" + resValue "string", "nfc_aid", "$NFC_AID_XW4d" } xw4SMS { @@ -142,6 +155,8 @@ android { buildConfigField "boolean", "RELAYINVITE_SUPPORTED", "false" buildConfigField "String", "VARIANT_NAME", "\"FOSS\"" buildConfigField "int", "VARIANT_CODE", "5" + buildConfigField "String", "NFC_AID", "\"${NFC_AID_XW4}\"" + resValue "string", "nfc_aid", "$NFC_AID_XW4" } // WARNING: "all" breaks things. Seems to be a keyword. Need @@ -390,8 +405,8 @@ task makeBuildAssets() { out += "date: ${date}\n" // I want the variant, but that's harder. Here's a quick hack from SO. - String target = gradle.startParameter.taskNames[-1] - out += "target: ${target}\n" + // String target = gradle.startParameter.taskNames[0] + // out += "target: ${target}\n" String diff = "git diff".execute().text.trim() if (diff) { diff --git a/xwords4/android/app/src/main/AndroidManifest.xml b/xwords4/android/app/src/main/AndroidManifest.xml index 6916ec874..4043fd8ab 100644 --- a/xwords4/android/app/src/main/AndroidManifest.xml +++ b/xwords4/android/app/src/main/AndroidManifest.xml @@ -32,10 +32,10 @@ - + - - - - - - @@ -213,5 +207,14 @@ + + + + + + + diff --git a/xwords4/android/app/src/main/assets/changes.html b/xwords4/android/app/src/main/assets/changes.html index eb0a0bc68..ab832c830 100644 --- a/xwords4/android/app/src/main/assets/changes.html +++ b/xwords4/android/app/src/main/assets/changes.html @@ -39,7 +39,7 @@

Next up

    -
  • Look for a workaround to allow NFC on Android 10
  • +
  • Fix email invitations
  • Support duplicate-style play (popular in France)
  • Improve play-by-data-sms workaround using NBSProxy
  • diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java index c0ba93ad0..4bc093d54 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java @@ -771,14 +771,14 @@ public class BTService extends XWJIService { { Context context = XWApp.getContext(); ConnStatusHandler - .updateStatusOut( context, null, CommsConnType.COMMS_CONN_BT, success ); + .updateStatusOut( context, CommsConnType.COMMS_CONN_BT, success ); } private static void updateStatusIn( boolean success ) { Context context = XWApp.getContext(); ConnStatusHandler - .updateStatusIn( context, null, CommsConnType.COMMS_CONN_BT, success ); + .updateStatusIn( context, CommsConnType.COMMS_CONN_BT, success ); } private static class KillerIn extends Thread implements AutoCloseable { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java index 8ec61e6b9..a0a0bc594 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java @@ -72,12 +72,13 @@ import org.eehouse.android.xw4.jni.XwJNI.GamePtr; import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.TilePickAlert.TilePickState; +import org.eehouse.android.xw4.NFCCardService.Wrapper; public class BoardDelegate extends DelegateBase implements TransportProcs.TPMsgHandler, View.OnClickListener, DwnldDelegate.DownloadFinishedListener, ConnStatusHandler.ConnStatusCBacks, - NFCUtils.NFCActor { + Wrapper.Procs { private static final String TAG = BoardDelegate.class.getSimpleName(); private static final int SCREEN_ON_TIME = 10 * 60 * 1000; // 10 mins @@ -126,6 +127,8 @@ public class BoardDelegate extends DelegateBase private DBAlert m_inviteAlert; private boolean m_haveStartedShowing; + private Wrapper mNFCWrapper; + public class TimerRunnable implements Runnable { private int m_why; private int m_when; @@ -170,7 +173,7 @@ public class BoardDelegate extends DelegateBase private boolean alertOrderAt( StartAlertOrder ord ) { boolean result = m_mySIS.mAlertOrder == ord; - Log.d( TAG, "alertOrderAt(%s) => %b", ord, result ); + // Log.d( TAG, "alertOrderAt(%s) => %b", ord, result ); return result; } @@ -558,6 +561,9 @@ public class BoardDelegate extends DelegateBase m_isFirstLaunch = null == savedInstanceState; getBundledData( savedInstanceState ); + int devID = DevID.getNFCDevID( m_activity ); + mNFCWrapper = Wrapper.init( m_activity, this, devID ); + m_utils = new BoardUtilCtxt(); m_timers = new TimerRunnable[4]; // needs to be in sync with // XWTimerReason @@ -601,9 +607,6 @@ public class BoardDelegate extends DelegateBase m_jniThreadRef.setDaemonOnce( true ); m_jniThreadRef.startOnce(); - // Don't seem to need to unregister... - NFCUtils.register( m_activity, BoardDelegate.this ); - setBackgroundColor(); setKeepScreenOn(); @@ -633,6 +636,7 @@ public class BoardDelegate extends DelegateBase protected void onResume() { super.onResume(); + Wrapper.setResumed( mNFCWrapper, true ); if ( null != m_jniThreadRef ) { doResume( false ); } else { @@ -642,6 +646,7 @@ public class BoardDelegate extends DelegateBase protected void onPause() { + Wrapper.setResumed( mNFCWrapper, false ); closeIfFinishing( false ); m_handler = null; ConnStatusHandler.setHandler( null ); @@ -1098,7 +1103,8 @@ public class BoardDelegate extends DelegateBase launchLookup( m_mySIS.words, m_gi.dictLang ); break; case NFC_TO_SELF: - GamesListDelegate.sendNFCToSelf( m_activity, makeNFCMessage() ); + Assert.assertFalse( BuildConfig.DEBUG ); + // GamesListDelegate.sendNFCToSelf( m_activity, makeNFCMessage() ); break; case DROP_RELAY_ACTION: dropConViaAndRestart(CommsConnType.COMMS_CONN_RELAY); @@ -1515,22 +1521,49 @@ public class BoardDelegate extends DelegateBase } ////////////////////////////////////////////////// - // NFCUtils.NFCActor + // ConnStatusHandler.ConnStatusCBacks ////////////////////////////////////////////////// @Override - public String makeNFCMessage() + public void invalidateParent() { - Log.d( TAG, "makeNFCMessage(): m_mySIS.nMissing: %d", m_mySIS.nMissing ); - String data = null; + runOnUiThread(new Runnable() { + @Override + public void run() { + m_view.invalidate(); + } + }); + } + + @Override + public void onStatusClicked() + { + onStatusClicked( m_jniGamePtr ); + } + + @Override + public Handler getHandler() + { + return m_handler; + } + + //////////////////////////////////////////////////////////// + // NFCCardService.Wrapper.Procs + //////////////////////////////////////////////////////////// + @Override + public void onReadingChange( boolean nowReading ) + { + // Do we need this? + } + + private byte[] getInvite() + { + byte[] result = null; if ( 0 < m_mySIS.nMissing // Isn't there a better test?? && DeviceRole.SERVER_ISSERVER == m_gi.serverRole ) { - Log.d( TAG, "makeNFCMessage(): invite case" ); NetLaunchInfo nli = new NetLaunchInfo( m_gi ); Assert.assertTrue( 0 <= m_nGuestDevs ); nli.forceChannel = 1 + m_nGuestDevs; - Assert.assertTrue( m_connTypes.contains( CommsConnType.COMMS_CONN_NFC ) ); - for ( Iterator iter = m_connTypes.iterator(); iter.hasNext(); ) { CommsConnType typ = iter.next(); @@ -1558,46 +1591,9 @@ public class BoardDelegate extends DelegateBase typ.toString() ); } } - data = nli.makeLaunchJSON(); - if ( null != data ) { - recordInviteSent( InviteMeans.NFC, null ); - } - } else if ( BuildConfig.MOVE_VIA_NFC ) { - Log.d( TAG, "makeNFCMessage(): move case" ); - byte[][] msgs = XwJNI.comms_getPending( m_jniGamePtr ); - data = NFCUtils.makeMsgsJSON( m_gi.gameID, msgs ); - } else { - Log.d( TAG, "makeNFCMessage(): other (bad!!) case" ); - Assert.assertFalse( BuildConfig.DEBUG ); + result = nli.asByteArray(); } - Log.d( TAG, "makeNFCMessage() => %s", data ); - return data; - } - - ////////////////////////////////////////////////// - // ConnStatusHandler.ConnStatusCBacks - ////////////////////////////////////////////////// - @Override - public void invalidateParent() - { - runOnUiThread(new Runnable() { - @Override - public void run() { - m_view.invalidate(); - } - }); - } - - @Override - public void onStatusClicked() - { - onStatusClicked( m_jniGamePtr ); - } - - @Override - public Handler getHandler() - { - return m_handler; + return result; } private void launchPhoneNumberInvite( int nMissing, SentInvitesInfo info, @@ -2269,8 +2265,15 @@ public class BoardDelegate extends DelegateBase if ( null == m_jniThread ) { m_jniThread = m_jniThreadRef.retain(); m_gi = m_jniThread.getGI(); + m_summary = m_jniThread.getSummary(); + Wrapper.setGameID( mNFCWrapper, m_gi.gameID ); + byte[] invite = getInvite(); + if ( null != invite ) { + NFCUtils.addInvitationFor( invite, m_gi.gameID ); + } + m_view.startHandling( m_activity, m_jniThread, m_connTypes ); handleViaThread( JNICmd.CMD_START ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/CommsTransport.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/CommsTransport.java index 9cbab9934..2b1eeb1b7 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/CommsTransport.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/CommsTransport.java @@ -181,7 +181,7 @@ public class CommsTransport implements TransportProcs, addIncoming(); } ConnStatusHandler. - updateStatusIn( m_context, null, + updateStatusIn( m_context, CommsConnType.COMMS_CONN_RELAY, 0 <= nRead ); } @@ -190,7 +190,7 @@ public class CommsTransport implements TransportProcs, if ( null != m_bytesOut ) { int nWritten = channel.write( m_bytesOut ); ConnStatusHandler. - updateStatusOut( m_context, null, + updateStatusOut( m_context, CommsConnType.COMMS_CONN_RELAY, 0 < nWritten ); } @@ -444,6 +444,7 @@ public class CommsTransport implements TransportProcs, .sendPacket( context, addr.p2p_addr, gameID, buf ); break; case COMMS_CONN_NFC: + nSent = NFCUtils.addMsgFor( buf, gameID ); break; default: Assert.fail(); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java index bd3513dc4..1b85b209f 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java @@ -178,10 +178,6 @@ public class ConnStatusHandler { R.string.connstat_net_fmt, connTypes.toString( context, true ))); for ( CommsConnType typ : connTypes.getTypes() ) { - if ( ! typ.isSelectable() ) { - continue; - } - sb.append( String.format( "\n\n*** %s ", typ.longName( context ) ) ); String did = addDebugInfo( context, gamePtr, addr, typ ); if ( null != did ) { @@ -363,12 +359,23 @@ public class ConnStatusHandler { updateStatusImpl( context, cbacks, connType, success, true ); } + public static void updateStatusIn( Context context, CommsConnType connType, + boolean success ) + { + updateStatusImpl( context, null, connType, success, true ); + } + public static void updateStatusOut( Context context, ConnStatusCBacks cbacks, CommsConnType connType, boolean success ) { updateStatusImpl( context, cbacks, connType, success, false ); } + public static void updateStatusOut( Context context, CommsConnType connType, boolean success ) + { + updateStatusImpl( context, null, connType, success, false ); + } + private static void updateStatusImpl( Context context, ConnStatusCBacks cbacks, CommsConnType connType, boolean success, boolean isIn ) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java index ba0c50222..e4fcc33ff 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DbgUtils.java @@ -158,14 +158,18 @@ public class DbgUtils { // return TextUtils.join( ", ", asStrs ); // } - // public static String hexDump( byte[] bytes ) - // { - // StringBuilder dump = new StringBuilder(); - // for ( byte byt : bytes ) { - // dump.append( String.format( "%02x ", byt ) ); - // } - // return dump.toString(); - // } + public static String hexDump( byte[] bytes ) + { + String result = ""; + if ( null != bytes ) { + StringBuilder dump = new StringBuilder(); + for ( byte byt : bytes ) { + dump.append( String.format( "%02x ", byt ) ); + } + result = dump.toString(); + } + return result; + } private static List sLockHolders = new ArrayList<>(); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java index 2778b3187..ac36e8936 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java @@ -37,6 +37,7 @@ public class DevID { private static final String DEVID_KEY = "DevID.devid_key"; private static final String DEVID_ACK_KEY = "key_relay_regid_ackd2"; private static final String FCM_REGVERS_KEY = "key_fcmvers_regid"; + private static final String NFC_DEVID_KEY = "key_nfc_devid"; private static String s_relayDevID; private static int s_asInt; @@ -145,4 +146,22 @@ public class DevID { { DBUtils.setBoolFor( context, DEVID_ACK_KEY, false ); } + + // Just a random number I hang onto as long as possible + private static int[] sNFCDevID = {0}; + public static int getNFCDevID( Context context ) + { + synchronized ( sNFCDevID ) { + if ( 0 == sNFCDevID[0] ) { + int devid = DBUtils.getIntFor( context, NFC_DEVID_KEY, 0 ); + if ( 0 == devid ) { + devid = Utils.nextRandomInt(); + DBUtils.setIntFor( context, NFC_DEVID_KEY, devid ); + } + sNFCDevID[0] = devid; + } + Log.d( TAG, "getNFCDevID() => %d", sNFCDevID[0] ); + return sNFCDevID[0]; + } + } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java index 5d8b342da..9390244f8 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java @@ -249,7 +249,7 @@ public class GameListItem extends LinearLayout case R.string.game_summary_field_empty: break; case R.string.game_summary_field_gameid: - value = String.format( "%X", m_summary.gameID ); + value = String.format( "%d", m_summary.gameID ); break; case R.string.game_summary_field_rowid: value = String.format( "%d", m_rowid ); 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 6c66aebc3..ba91df632 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 @@ -64,6 +64,7 @@ import org.eehouse.android.xw4.loc.LocUtils; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.Iterator; @@ -2353,14 +2354,14 @@ public class GamesListDelegate extends ListDelegateBase private boolean tryNFCIntent( Intent intent ) { boolean result = false; - String data = NFCUtils.getFromIntent( intent ); + byte[] data = NFCUtils.getFromIntent( intent ); if ( null != data ) { NetLaunchInfo nli = NetLaunchInfo.makeFrom( m_activity, data ); if ( null != nli && nli.isValid() ) { startNewNetGame( nli ); result = true; } else { - NFCUtils.receiveMsgs( m_activity, data ); + Assert.assertFalse( BuildConfig.DEBUG ); } } return result; @@ -2843,10 +2844,12 @@ public class GamesListDelegate extends ListDelegateBase ; } - public static void sendNFCToSelf( Context context, String data ) + public static void postNFCInvite( Context context, byte[] data ) { - Intent intent = makeSelfIntent( context ); - NFCUtils.populateIntent( intent, data ); + Intent intent = makeSelfIntent( context ) + .addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ) + ; + NFCUtils.populateIntent( context, intent, data ); context.startActivity( intent ); } 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 bb1a7cdf5..999e03c74 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 @@ -79,6 +79,12 @@ public class MultiMsgSink implements TransportProcs { .sendPacket( m_context, addr.p2p_addr, gameID, buf ); } + int sendViaNFC( byte[] buf, int gameID ) + { + Log.d( TAG, "sendViaNFC(gameID=%d, len=%d)", gameID, buf.length ); + return NFCUtils.addMsgFor( buf, gameID ); + } + public int numSent() { return m_sentSet.size(); @@ -108,12 +114,13 @@ public class MultiMsgSink implements TransportProcs { break; case COMMS_CONN_NFC: Log.d( TAG, "transportSend(): got for NFC" ); + nSent = sendViaNFC( buf, gameID ); break; default: Assert.fail(); break; } - Log.i( TAG, "transportSend(): sent %d msgs for game %d/%x via %s", + Log.i( TAG, "transportSend(): sent %d bytes for game %d/%x via %s", nSent, gameID, gameID, typ.toString() ); if ( 0 < nSent ) { Log.d( TAG, "transportSend: adding %s", msgID ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NBSProto.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NBSProto.java index 65c6100ca..2394824ee 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NBSProto.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NBSProto.java @@ -66,8 +66,7 @@ public class NBSProto { DbgUtils.showf( context, "Got msg %d", s_nReceived ); } - ConnStatusHandler.updateStatusIn( context, null, - CommsConnType.COMMS_CONN_SMS, + ConnStatusHandler.updateStatusIn( context, CommsConnType.COMMS_CONN_SMS, true ); } @@ -380,8 +379,7 @@ public class NBSProto { DbgUtils.showf( context, "Sent msg %d", s_nSent ); } - ConnStatusHandler.updateStatusOut( context, null, - CommsConnType.COMMS_CONN_SMS, + ConnStatusHandler.updateStatusOut( context, CommsConnType.COMMS_CONN_SMS, success ); } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java new file mode 100644 index 000000000..f87d624df --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java @@ -0,0 +1,795 @@ +/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */ +/* + * Copyright 2019 by Eric House (xwords@eehouse.org). All rights reserved. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +package org.eehouse.android.xw4; + +import android.app.Activity; +import android.content.Context; +import android.nfc.NfcAdapter; +import android.nfc.Tag; +import android.nfc.cardemulation.HostApduService; +import android.nfc.tech.IsoDep; +import android.os.Bundle; +import android.text.TextUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eehouse.android.xw4.NFCUtils.MsgToken; +import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; + +public class NFCCardService extends HostApduService { + private static final String TAG = NFCCardService.class.getSimpleName(); + private static final boolean USE_BIGINTEGER = true; + private static final int LEN_OFFSET = 4; + private static final byte VERSION_1 = (byte)0x01; + + private int mMyDevID; + + private static enum HEX_STR { + DEFAULT_CLA( "00" ) + , SELECT_INS( "A4" ) + , STATUS_FAILED( "6F00" ) + , CLA_NOT_SUPPORTED( "6E00" ) + , INS_NOT_SUPPORTED( "6D00" ) + , STATUS_SUCCESS( "9000" ) + , CMD_MSG_PART( "70FC" ) + ; + + private byte[] mBytes; + private HEX_STR( String hex ) { mBytes = Utils.hexStr2ba(hex); } + private byte[] asBA() { return mBytes; } + private boolean matchesFrom( byte[] src ) + { + return matchesFrom( src, 0 ); + } + private boolean matchesFrom( byte[] src, int offset ) + { + boolean result = offset + mBytes.length <= src.length; + for ( int ii = 0; result && ii < mBytes.length; ++ii ) { + result = src[offset + ii] == mBytes[ii]; + } + // Log.d( TAG, "%s.matchesFrom(%s) => %b", this, src, result ); + return result; + } + int length() { return asBA().length; } + } + + private static int sNextMsgID = 0; + private static synchronized int getNextMsgID() + { + return ++sNextMsgID; + } + + private static byte[] numTo( int num ) + { + byte[] result; + if ( USE_BIGINTEGER ) { + BigInteger bi = BigInteger.valueOf( num ); + byte[] bibytes = bi.toByteArray(); + result = new byte[1 + bibytes.length]; + result[0] = (byte)bibytes.length; + System.arraycopy( bibytes, 0, result, 1, bibytes.length ); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream( baos ); + try { + dos.writeInt( num ); + dos.flush(); + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + result = baos.toByteArray(); + } + // Log.d( TAG, "numTo(%d) => %s", num, DbgUtils.hexDump(result) ); + return result; + } + + private static int numFrom( ByteArrayInputStream bais ) throws IOException + { + int biLen = bais.read(); + // Log.d( TAG, "numFrom(): read biLen: %d", biLen ); + byte[] bytes = new byte[biLen]; + bais.read( bytes ); + BigInteger bi = new BigInteger( bytes ); + int result = bi.intValue(); + + // Log.d( TAG, "numFrom() => %d", result ); + return result; + } + + private static int numFrom( byte[] bytes, int start, int out[] ) + { + int result; + if ( USE_BIGINTEGER ) { + byte biLen = bytes[start]; + byte[] rest = Arrays.copyOfRange( bytes, start + 1, start + 1 + biLen ); + BigInteger bi = new BigInteger(rest); + out[0] = bi.intValue(); + result = biLen + 1; + } else { + ByteArrayInputStream bais = new ByteArrayInputStream( bytes, start, + bytes.length - start ); + DataInputStream dis = new DataInputStream( bais ); + try { + out[0] = dis.readInt(); + } catch ( IOException ioe ) { + Log.e( TAG, "from readInt(): %s", ioe.getMessage() ); + } + result = bais.available() - start; + } + return result; + } + + private static void testNumThing() + { + Log.d( TAG, "testNumThing() starting" ); + + int[] out = {0}; + for ( int ii = 1; ii > 0 && ii < Integer.MAX_VALUE; ii *= 2 ) { + byte[] tmp = numTo( ii ); + numFrom( tmp, 0, out ); + if ( ii != out[0] ) { + Log.d( TAG, "testNumThing(): %d failed; got %d", ii, out[0] ); + break; + } else { + Log.d( TAG, "testNumThing(): %d ok", ii ); + } + } + Log.d( TAG, "testNumThing() DONE" ); + } + + private static class QueueElem { + Context context; + byte[] msg; + QueueElem( Context pContext, byte[] pMsg ) { + context = pContext; + msg = pMsg; + } + } + + private static LinkedBlockingQueue sQueue = null; + + private synchronized static void addToMsgThread( Context context, byte[] msg ) + { + if ( 0 < msg.length ) { + QueueElem elem = new QueueElem( context, msg ); + if ( null == sQueue ) { + sQueue = new LinkedBlockingQueue<>(); + new Thread( new Runnable() { + @Override + public void run() { + Log.d( TAG, "addToMsgThread(): run starting" ); + for ( ; ; ) { + try { + QueueElem elem = sQueue.take(); + NFCUtils.receiveMsgs( elem.context, elem.msg ); + updateStatus( elem.context, true ); + } catch ( InterruptedException ie ) { + break; + } + } + Log.d( TAG, "addToMsgThread(): run exiting" ); + } + } ).start(); + } + sQueue.add( elem ); + // } else { + // // This is very common right now + // Log.d( TAG, "addToMsgThread(): dropping 0-length msg" ); + } + } + + private static void updateStatus( Context context, boolean in ) + { + if ( in ) { + ConnStatusHandler + .updateStatusIn( context, CommsConnType.COMMS_CONN_NFC, true ); + } else { + ConnStatusHandler + .updateStatusOut( context, CommsConnType.COMMS_CONN_NFC, true ); + } + } + + // Remove this once we don't need logging to confirm stuff's loading + @Override + public void onCreate() + { + super.onCreate(); + mMyDevID = DevID.getNFCDevID( this ); + Log.d( TAG, "onCreate() got mydevid %d", mMyDevID ); + } + + private int mGameID; + + @Override + public byte[] processCommandApdu( byte[] apdu, Bundle extras ) + { + // Log.d( TAG, "processCommandApdu(%s)", DbgUtils.hexDump(apdu ) ); + + HEX_STR resStr = HEX_STR.STATUS_FAILED; + boolean isAidCase = false; + + if ( null != apdu ) { + if ( HEX_STR.CMD_MSG_PART.matchesFrom( apdu ) ) { + resStr = HEX_STR.STATUS_SUCCESS; + int[] msgID = {0}; + byte[] all = reassemble( this, apdu, msgID, HEX_STR.CMD_MSG_PART ); + if ( null != all ) { + addToMsgThread( this, all ); + setLatestAck( msgID[0] ); + } + } else { + Log.d( TAG, "processCommandApdu(): aid case?" ); + if ( ! HEX_STR.DEFAULT_CLA.matchesFrom( apdu ) ) { + resStr = HEX_STR.CLA_NOT_SUPPORTED; + } else if ( ! HEX_STR.SELECT_INS.matchesFrom( apdu, 1 ) ) { + resStr = HEX_STR.INS_NOT_SUPPORTED; + } else if ( LEN_OFFSET >= apdu.length ) { + Log.d( TAG, "processCommandApdu(): apdu too short" ); + // Not long enough for length byte + } else { + try { + ByteArrayInputStream bais + = new ByteArrayInputStream( apdu, LEN_OFFSET, + apdu.length - LEN_OFFSET ); + byte aidLen = (byte)bais.read(); + Log.d( TAG, "aidLen=%d", aidLen ); + if ( bais.available() >= aidLen + 1 ) { + byte[] aidPart = new byte[aidLen]; + bais.read( aidPart ); + String aidStr = Utils.ba2HexStr( aidPart ); + if ( BuildConfig.NFC_AID.equals( aidStr ) ) { + byte minVersion = (byte)bais.read(); + byte maxVersion = (byte)bais.read(); + if ( minVersion == VERSION_1 ) { + int devID = numFrom( bais ); + Log.d( TAG, "processCommandApdu(): read " + + "remote devID: %d", devID ); + mGameID = numFrom( bais ); + Log.d( TAG, "read gameID: %d", mGameID ); + if ( 0 < bais.available() ) { + Log.d( TAG, "processCommandApdu(): " + + "leaving anything behind?" ); + } + resStr = HEX_STR.STATUS_SUCCESS; + isAidCase = true; + } else { + Log.e( TAG, "unexpected version %d; I'm too old?", + minVersion ); + } + } else { + Log.e( TAG, "aid mismatch: got %s but wanted %s", + aidStr, BuildConfig.NFC_AID ); + } + } + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + } + } + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + baos.write( resStr.asBA() ); + if ( HEX_STR.STATUS_SUCCESS == resStr ) { + if ( isAidCase ) { + baos.write( VERSION_1 ); // min + baos.write( numTo( mMyDevID ) ); + } else { + MsgToken token = NFCUtils.getMsgsFor( mGameID ); + byte[][] tmp = wrapMsg( token, Short.MAX_VALUE ); + Assert.assertTrue( 1 == tmp.length || !BuildConfig.DEBUG ); + baos.write( tmp[0] ); + } + } + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + byte[] result = baos.toByteArray(); + + Log.d( TAG, "processCommandApdu(%s) => %s", DbgUtils.hexDump( apdu ), + DbgUtils.hexDump( result ) ); + // this comes out of transceive() below!!! + return result; + } // processCommandApdu + + @Override + public void onDeactivated( int reason ) + { + String str = ""; + switch ( reason ) { + case HostApduService.DEACTIVATION_LINK_LOSS: + str = "DEACTIVATION_LINK_LOSS"; + break; + case HostApduService.DEACTIVATION_DESELECTED: + str = "DEACTIVATION_DESELECTED"; + break; + } + + Log.d( TAG, "onDeactivated(reason=%s)", str ); + } + + private static Map sSentTokens = new HashMap<>(); + private static void removeSentMsgs( Context context, int ack ) + { + MsgToken msgs = null; + if ( 0 != ack ) { + Log.d( TAG, "removeSentMsgs(msgID=%d)", ack ); + synchronized ( sSentTokens ) { + msgs = sSentTokens.remove( ack ); + Log.d( TAG, "removeSentMsgs(): removed %s, now have %s", msgs, keysFor() ); + } + updateStatus( context, false ); + } + if ( null != msgs ) { + msgs.removeSentMsgs(); + } + } + + private static void remember( int msgID, MsgToken msgs ) + { + if ( 0 != msgID ) { + Log.d( TAG, "remember(msgID=%d)", msgID ); + synchronized ( sSentTokens ) { + sSentTokens.put( msgID, msgs ); + Log.d( TAG, "remember(): now have %s", keysFor() ); + } + } + } + + private static String keysFor() + { + String result = ""; + if ( BuildConfig.DEBUG ) { + result = TextUtils.join( ",", sSentTokens.keySet() ); + } + return result; + } + + private static byte[][] sParts = null; + private static int sMsgID = 0; + private synchronized static byte[] reassemble( Context context, byte[] part, + int[] msgIDOut, HEX_STR cmd ) + { + return reassemble( context, part, msgIDOut, cmd.length() ); + } + + private synchronized static byte[] reassemble( Context context, byte[] part, + int[] msgIDOut, int offset ) + { + part = Arrays.copyOfRange( part, offset, part.length ); + return reassemble( context, part, msgIDOut ); + } + + private synchronized static byte[] reassemble( Context context, + byte[] part, int[] msgIDOut ) + { + byte[] result = null; + try { + ByteArrayInputStream bais = new ByteArrayInputStream( part ); + + final int cur = bais.read(); + final int count = bais.read(); + if ( 0 == cur ) { + sMsgID = numFrom( bais ); + int ack = numFrom( bais ); + removeSentMsgs( context, ack ); + } + + boolean inSequence = true; + if ( sParts == null ) { + if ( 0 == cur ) { + sParts = new byte[count][]; + } else { + Log.e( TAG, "reassemble(): out-of-order message 1" ); + inSequence = false; + } + } else if ( cur >= count || count != sParts.length || null != sParts[cur] ) { + // result = HEX_STR.STATUS_FAILED; + inSequence = false; + Log.e( TAG, "reassemble(): out-of-order message 2" ); + } + + if ( !inSequence ) { + sParts = null; // so we can try again later + } else { + // write rest into array + byte[] rest = new byte[bais.available()]; + bais.read( rest, 0, rest.length ); + sParts[cur] = rest; + // Log.d( TAG, "addOrProcess(): added elem %d: %s", cur, DbgUtils.hexDump( rest ) ); + + // Done? Process!! + if ( cur + 1 == count ) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for ( int ii = 0; ii < sParts.length; ++ii ) { + baos.write( sParts[ii] ); + } + sParts = null; + + result = baos.toByteArray(); + msgIDOut[0] = sMsgID; + if ( 0 != sMsgID ) { + Log.d( TAG, "reassemble(): done reassembling msgID=%d: %s", + msgIDOut[0], DbgUtils.hexDump(result) ); + } + } + } + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + return result; + } + + private static AtomicInteger sLatestAck = new AtomicInteger(0); + private static int getLatestAck() + { + int result = sLatestAck.getAndSet(0); + if ( 0 != result ) { + Log.d( TAG, "getLatestAck() => %d", result ); + } + return result; + } + + private static void setLatestAck( int ack ) + { + if ( 0 != ack ) { + Log.e( TAG, "setLatestAck(%d)", ack ); + } + int oldVal = sLatestAck.getAndSet( ack ); + if ( 0 != oldVal ) { + Log.e( TAG, "setLatestAck(%d): dropping ack msgID %d", ack, oldVal ); + } + } + + private static final int HEADER_SIZE = 10; + private static byte[][] wrapMsg( MsgToken token, int maxLen ) + { + byte[] msg = token.getMsgs(); + final int length = null == msg ? 0 : msg.length; + final int msgID = (0 == length) ? 0 : getNextMsgID(); + if ( 0 < msgID ) { + Log.d( TAG, "wrapMsg(%s); msgID=%d", DbgUtils.hexDump( msg ), msgID ); + } + final int count = 1 + (length / (maxLen - HEADER_SIZE)); + byte[][] result = new byte[count][]; + try { + int offset = 0; + for ( int ii = 0; ii < count; ++ii ) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write( HEX_STR.CMD_MSG_PART.asBA() ); + baos.write( (byte)ii ); + baos.write( (byte)count ); + if ( 0 == ii ) { + baos.write( numTo( msgID ) ); + int latestAck = getLatestAck(); + baos.write( numTo( latestAck ) ); + } + Assert.assertTrue( HEADER_SIZE >= baos.toByteArray().length ); + + int thisLen = Math.min( maxLen - HEADER_SIZE, length - offset ); + if ( 0 < thisLen ) { + // Log.d( TAG, "writing %d bytes starting from offset %d", + // thisLen, offset ); + baos.write( msg, offset, thisLen ); + offset += thisLen; + } + byte[] tmp = baos.toByteArray(); + // Log.d( TAG, "wrapMsg(): adding res[%d]: %s", ii, DbgUtils.hexDump(tmp) ); + result[ii] = tmp; + } + remember( msgID, token ); + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + return result; + } + + public static class Wrapper implements NfcAdapter.ReaderCallback, + NFCUtils.HaveDataListener { + private Activity mActivity; + private boolean mHaveData; + private Procs mProcs; + private NfcAdapter mAdapter; + private int mMinMS = 300; + private int mMaxMS = 500; + private boolean mConnected = false; + private int mMyDevID; + + public interface Procs { + void onReadingChange( boolean nowReading ); + } + + public static Wrapper init( Activity activity, Procs procs, int devID ) + { + Wrapper instance = null; + if ( null != NfcAdapter.getDefaultAdapter( activity ) ) { + instance = new Wrapper( activity, procs, devID ); + } + Log.d( TAG, "Wrapper.init(devID=%d) => %s", devID, instance ); + return instance; + } + + static void setResumed( Wrapper instance, boolean resumed ) + { + if ( null != instance ) { + instance.setResumed( resumed ); + } + } + + static void setGameID( Wrapper instance, int gameID ) + { + if ( null != instance ) { + instance.setGameID( gameID ); + } + } + + private Wrapper( Activity activity, Procs procs, int devID ) + { + mActivity = activity; + mProcs = procs; + mMyDevID = devID; + mAdapter = NfcAdapter.getDefaultAdapter( activity ); + } + + private void setResumed( boolean resumed ) + { + if ( resumed ) { + startReadModeThread(); + } else { + stopReadModeThread(); + } + } + + @Override + public void onHaveDataChanged( boolean haveData ) + { + if ( mHaveData != haveData ) { + mHaveData = haveData; + Log.d( TAG, "onHaveDataChanged(): mHaveData now %b", mHaveData ); + interruptThread(); + } + } + + private boolean haveData() + { + boolean result = mHaveData; + // Log.d( TAG, "haveData() => %b", result ); + return result; + } + + private int mGameID; + private void setGameID( int gameID ) + { + Log.d( TAG, "setGameID(%d)", gameID ); + mGameID = gameID; + NFCUtils.setHaveDataListener( gameID, this ); + interruptThread(); + } + + private void interruptThread() + { + synchronized ( mThreadRef ) { + if ( null != mThreadRef[0] ) { + mThreadRef[0].interrupt(); + } + } + } + + @Override + public void onTagDiscovered( Tag tag ) + { + mConnected = true; + IsoDep isoDep = IsoDep.get( tag ); + try { + isoDep.connect(); + int maxLen = isoDep.getMaxTransceiveLength(); + Log.d( TAG, "onTagDiscovered() connected; max len: %d", maxLen ); + byte[] aidBytes = Utils.hexStr2ba( BuildConfig.NFC_AID ); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write( Utils.hexStr2ba( "00A40400" ) ); + baos.write( (byte)aidBytes.length ); + baos.write( aidBytes ); + baos.write( VERSION_1 ); // min + baos.write( VERSION_1 ); // max + baos.write( numTo( mMyDevID ) ); + baos.write( numTo( mGameID ) ); + byte[] msg = baos.toByteArray(); + Assert.assertTrue( msg.length < maxLen || !BuildConfig.DEBUG ); + byte[] response = isoDep.transceive( msg ); + + // The first reply from transceive() is special. If it starts + // with STATUS_SUCCESS then it also includes the version we'll + // be using to communicate, either what we sent over or + // something lower (for older code on the other side), and the + // remote's deviceID + if ( HEX_STR.STATUS_SUCCESS.matchesFrom( response ) ) { + int offset = HEX_STR.STATUS_SUCCESS.length(); + byte version = response[offset++]; + if ( version == VERSION_1 ) { + int[] out = {0}; + offset += numFrom( response, offset, out ); + Log.d( TAG, "onTagDiscovered(): read remote devID: %d", + out[0] ); + runMessageLoop( isoDep, maxLen ); + } else { + Log.e( TAG, "onTagDiscovered(): remote sent version %d, " + + "not %d; exiting", version, VERSION_1 ); + } + } + isoDep.close(); + } catch ( IOException ioe ) { + Log.e( TAG, "got ioe: " + ioe.getMessage() ); + } + + mConnected = false; + interruptThread(); // make sure we leave read mode! + Log.d( TAG, "onTagDiscovered() DONE" ); + } + + private void runMessageLoop( IsoDep isoDep, int maxLen ) throws IOException + { + outer: + for ( ; ; ) { + MsgToken token = NFCUtils.getMsgsFor( mGameID ); + // PENDING: no need for this Math.min thing once well tested + byte[][] toFit = wrapMsg( token, Math.min( 50, maxLen ) ); + for ( int ii = 0; ii < toFit.length; ++ii ) { + byte[] one = toFit[ii]; + Assert.assertTrue( one.length < maxLen || !BuildConfig.DEBUG ); + byte[] response = isoDep.transceive( one ); + if ( ! receiveAny( response ) ) { + break outer; + } + } + } + } + + private boolean receiveAny( byte[] response ) + { + boolean statusOK = HEX_STR.STATUS_SUCCESS.matchesFrom( response ); + if ( statusOK ) { + int offset = HEX_STR.STATUS_SUCCESS.length(); + if ( HEX_STR.CMD_MSG_PART.matchesFrom( response, offset ) ) { + int[] msgID = {0}; + byte[] all = reassemble( mActivity, response, msgID, + offset + HEX_STR.CMD_MSG_PART.length() ); + if ( null != all ) { + addToMsgThread( mActivity, all ); + setLatestAck( msgID[0] ); + } + } + } + Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); + return statusOK; + } + + private class ReadModeThread extends Thread { + private boolean mShouldStop = false; + private boolean mInReadMode = false; + private final int mFlags = NfcAdapter.FLAG_READER_NFC_A + | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK; + + @Override + public void run() + { + Log.d( TAG, "ReadModeThread.run() starting" ); + Random random = new Random(); + + while ( !mShouldStop ) { + boolean wantReadMode = mConnected || !mInReadMode && haveData(); + if ( wantReadMode && !mInReadMode ) { + mAdapter.enableReaderMode( mActivity, Wrapper.this, mFlags, null ); + } else if ( mInReadMode && !wantReadMode ) { + mAdapter.disableReaderMode( mActivity ); + } + mInReadMode = wantReadMode; + Log.d( TAG, "run(): inReadMode now: %b", mInReadMode ); + + // Now sleep. If we aren't going to want to toggle read + // mode soon, sleep until interrupted by a state change, + // e.g. getting data or losing connection. + long intervalMS = Long.MAX_VALUE; + if ( (mInReadMode && !mConnected) || haveData() ) { + intervalMS = mMinMS + (Math.abs(random.nextInt()) + % (mMaxMS - mMinMS)); + } + try { + Thread.sleep( intervalMS ); + } catch ( InterruptedException ie ) { + Log.d( TAG, "run interrupted" ); + } + // toggle(); + // try { + // // How long to sleep. + // int intervalMS = mMinMS + (Math.abs(mRandom.nextInt()) + // % (mMaxMS - mMinMS)); + // // Log.d( TAG, "sleeping for %d ms", intervalMS ); + // Thread.sleep( intervalMS ); + // } catch ( InterruptedException ie ) { + // Log.d( TAG, "run interrupted" ); + // } + } + + // Kill read mode on the way out + if ( mInReadMode ) { + mAdapter.disableReaderMode( mActivity ); + mInReadMode = false; + } + + // Clear the reference only if it's me + synchronized ( mThreadRef ) { + if ( mThreadRef[0] == this ) { + mThreadRef[0] = null; + } + } + Log.d( TAG, "ReadModeThread.run() exiting" ); + } + + public void doStop() + { + mShouldStop = true; + interrupt(); + } + } + + private ReadModeThread[] mThreadRef = {null}; + private void startReadModeThread() + { + synchronized ( mThreadRef ) { + if ( null == mThreadRef[0] ) { + mThreadRef[0] = new ReadModeThread(); + mThreadRef[0].start(); + } + } + } + + private void stopReadModeThread() + { + ReadModeThread thread; + synchronized ( mThreadRef ) { + thread = mThreadRef[0]; + mThreadRef[0] = null; + } + + if ( null != thread ) { + thread.doStop(); + try { + thread.join(); + } catch ( InterruptedException ex ) { + Log.d( TAG, "stopReadModeThread(): %s", ex ); + } + } + } + } +} 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 02be6b688..56772b53a 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 @@ -25,20 +25,27 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; import android.nfc.NfcAdapter; import android.nfc.NfcEvent; import android.nfc.NfcManager; import android.os.Build; import android.os.Parcelable; -import org.json.JSONArray; -import org.json.JSONObject; -import org.json.JSONException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; -import org.eehouse.android.xw4.loc.LocUtils; +import org.eehouse.android.xw4.MultiService.MultiEvent; import org.eehouse.android.xw4.jni.CommsAddrRec; +import org.eehouse.android.xw4.loc.LocUtils; public class NFCUtils { private static final String TAG = NFCUtils.class.getSimpleName(); @@ -46,53 +53,14 @@ public class NFCUtils { private static final String NFC_TO_SELF_ACTION = "org.eehouse.nfc_to_self"; private static final String NFC_TO_SELF_DATA = "nfc_data"; - private static final String MSGS = "MSGS"; - private static final String GAMEID = "GAMEID"; + private static final byte MESSAGE = 0x01; + private static final byte INVITE = 0x02; + private static final byte REPLY = 0x03; - public interface NFCActor { - String makeNFCMessage(); - } + private static final byte REPLY_NOGAME = 0x00; - private static boolean s_inSDK; + private static boolean s_inSDK = 14 <= Build.VERSION.SDK_INT; private static boolean[] s_nfcAvail; - private static SafeNFC s_safeNFC; - static { - s_inSDK = 14 <= Build.VERSION.SDK_INT - && Build.VERSION.SDK_INT <= Build.VERSION_CODES.P; - if ( s_inSDK ) { - s_safeNFC = new SafeNFCImpl(); - } - } - - private static interface SafeNFC { - public void register( Activity activity, NFCActor actor ); - } - - private static class SafeNFCImpl implements SafeNFC { - public void register( final Activity activity, final NFCActor actor ) - { - NfcManager manager = - (NfcManager)activity.getSystemService( Context.NFC_SERVICE ); - if ( null != manager ) { - NfcAdapter adapter = manager.getDefaultAdapter(); - if ( null != adapter ) { - NfcAdapter.CreateNdefMessageCallback cb = - new NfcAdapter.CreateNdefMessageCallback() { - public NdefMessage createNdefMessage( NfcEvent evt ) - { - NdefMessage msg = null; - String data = actor.makeNFCMessage(); - if ( null != data ) { - msg = makeMessage( activity, data ); - } - return msg; - } - }; - adapter.setNdefPushMessageCallback( cb, activity ); - } - } - } - } // Return array of two booleans, the first indicating whether the // device supports NFC and the second whether it's on. Only the @@ -108,38 +76,32 @@ public class NFCUtils { if ( s_nfcAvail[0] ) { s_nfcAvail[1] = getNFCAdapter( context ).isEnabled(); } + // Log.d( TAG, "nfcAvail() => {%b,%b}", s_nfcAvail[0], s_nfcAvail[1] ); return s_nfcAvail; } - public static String getFromIntent( Intent intent ) + public static byte[] getFromIntent( Intent intent ) { - String result = null; + byte[] result = null; String action = intent.getAction(); - if ( NfcAdapter.ACTION_NDEF_DISCOVERED.equals( action ) ) { - Parcelable[] rawMsgs = - intent.getParcelableArrayExtra( NfcAdapter.EXTRA_NDEF_MESSAGES ); - // only one message sent during the beam - NdefMessage msg = (NdefMessage)rawMsgs[0]; - // record 0 contains the MIME type, record 1 is the AAR, if present - result = new String( msg.getRecords()[0].getPayload() ); - } else if ( NFC_TO_SELF_ACTION.equals( action ) ) { - result = intent.getStringExtra( NFC_TO_SELF_DATA ); + 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( Intent intent, String data ) + public static void populateIntent( Context context, Intent intent, + byte[] data ) { - intent.setAction( NFC_TO_SELF_ACTION ) - .putExtra( NFC_TO_SELF_DATA, data ); - } - - public static void register( Activity activity, NFCActor actor ) - { - if ( null != s_safeNFC ) { - s_safeNFC.register( activity, actor ); + NetLaunchInfo nli = NetLaunchInfo.makeFrom( context, data ); + if ( null != nli ) { + intent.setAction( NFC_TO_SELF_ACTION ) + .putExtra( NFC_TO_SELF_DATA, data ); + } else { + Assert.assertFalse( BuildConfig.DEBUG ); } } @@ -162,19 +124,6 @@ public class NFCUtils { .create(); } - private static NdefMessage makeMessage( Activity activity, String data ) - { - String mimeType = LocUtils.getString( activity, R.string.xwords_nfc_mime ); - NdefMessage msg = new NdefMessage( new NdefRecord[] { - new NdefRecord(NdefRecord.TNF_MIME_MEDIA, - mimeType.getBytes(), new byte[0], - data.getBytes()) - ,NdefRecord. - createApplicationRecord( activity.getPackageName() ) - }); - return msg; - } - private static NfcAdapter getNFCAdapter( Context context ) { NfcManager manager = @@ -182,44 +131,303 @@ public class NFCUtils { return manager.getDefaultAdapter(); } - static String makeMsgsJSON( int gameID, byte[][] msgs ) + private static byte[] formatMsgs( int gameID, List msgs ) { - String result = null; + return formatMsgs( gameID, msgs.toArray( new byte[msgs.size()][] ) ); + } - JSONArray arr = new JSONArray(); - for ( byte[] msg : msgs ) { - arr.put( Utils.base64Encode( msg ) ); - } - - try { - JSONObject obj = new JSONObject(); - obj.put( MSGS, arr ); - obj.put( GAMEID, gameID ); - - result = obj.toString(); - } catch ( JSONException ex ) { - Assert.assertFalse( BuildConfig.DEBUG ); + private static byte[] formatMsgs( int gameID, byte[][] msgs ) + { + byte[] result = null; + + if ( null != msgs && 0 < msgs.length ) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream( baos ); + dos.writeInt( gameID ); + Log.d( TAG, "formatMsgs(): wrote gameID: %d", gameID ); + dos.flush(); + baos.write( msgs.length ); + for ( int ii = 0; ii < msgs.length; ++ii ) { + byte[] msg = msgs[ii]; + short len = (short)msg.length; + baos.write( len & 0xFF ); + baos.write( (len >> 8) & 0xFF ); + baos.write( msg ); + } + result = baos.toByteArray(); + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } } + Log.d( TAG, "formatMsgs(gameID=%d) => %s", gameID, DbgUtils.hexDump( result ) ); return result; } - static boolean receiveMsgs( Context context, String data ) + private static byte[][] unformatMsgs( byte[] data, int start, int[] gameID ) { - Log.d( TAG, "receiveMsgs()" ); - int gameID[] = {0}; - byte[][] msgs = msgsFrom( data, gameID ); - boolean success = null != msgs && 0 < msgs.length; - if ( success ) { - NFCServiceHelper helper = new NFCServiceHelper( context ); - long[] rowids = DBUtils.getRowIDsFor( context, gameID[0] ); - for ( long rowid : rowids ) { - NFCMsgSink sink = new NFCMsgSink( context, rowid ); - for ( byte[] msg : msgs ) { - helper.receiveMessage( rowid, sink, msg ); + byte[][] result = null; + try { + ByteArrayInputStream bais + = new ByteArrayInputStream( data, start, data.length ); + DataInputStream dis = new DataInputStream( bais ); + gameID[0] = dis.readInt(); + Log.d( TAG, "unformatMsgs(): read gameID: %d", gameID[0] ); + int count = bais.read(); + Log.d( TAG, "unformatMsgs(): read count: %d", count ); + result = new byte[count][]; + + for ( int ii = 0; ii < count; ++ii ) { + short len = (short)bais.read(); + len |= (int)(bais.read() << 8); + Log.d( TAG, "unformatMsgs(): read len %d for msg %d", len, ii ); + byte[] msg = new byte[len]; + int nRead = bais.read( msg ); + Assert.assertTrue( nRead == msg.length ); + result[ii] = msg; + } + } catch ( IOException ex ) { + Log.d( TAG, "ex: %s: %s", ex, ex.getMessage() ); + result = null; + gameID[0] = 0; + } + Log.d( TAG, "unformatMsgs() => %s (len=%d)", result, + null == result ? 0 : result.length ); + return result; + } + + interface HaveDataListener { + void onHaveDataChanged( boolean nowHaveData ); + } + + public static class MsgToken { + private MsgsStore mStore; + private byte[][] mMsgs; + private int mGameID; + + private MsgToken( MsgsStore store, int gameID ) + { + mStore = store; + mGameID = gameID; + mMsgs = mStore.getMsgsFor( gameID ); + } + + byte[] getMsgs() + { + return formatMsgs( mGameID, mMsgs ); + } + + void removeSentMsgs() + { + mStore.removeSentMsgs( mGameID, mMsgs ); + } + } + + private static class MsgsStore { + private Map> mListeners + = new HashMap<>(); + private static Map> mMsgMap = new HashMap<>(); + + void setHaveDataListener( int gameID, HaveDataListener listener ) + { + Assert.assertFalse( gameID == 0 ); + WeakReference ref = new WeakReference<>(listener); + synchronized ( mListeners ) { + mListeners.put( gameID, ref ); + } + + byte[][] msgs = getMsgsFor( gameID ); + listener.onHaveDataChanged( null != msgs && 0 < msgs.length ); + } + + private int addMsgFor( int gameID, byte typ, byte[] msg ) + { + Boolean nowHaveData = null; + + synchronized ( mMsgMap ) { + if ( !mMsgMap.containsKey( gameID ) ) { + mMsgMap.put( gameID, new ArrayList() ); + } + List msgs = mMsgMap.get( gameID ); + + byte[] full = new byte[msg.length + 1]; + full[0] = typ; + System.arraycopy( msg, 0, full, 1, msg.length ); + + // Can't use msgs.contains() because it uses equals() + boolean isDuplicate = false; + for ( byte[] curMsg : msgs ) { + if ( Arrays.equals( curMsg, full ) ) { + isDuplicate = true; + break; + } + } + + if ( !isDuplicate ) { + msgs.add( full ); + nowHaveData = 0 < msgs.size(); + Log.d( TAG, "addMsgFor(gameID=%d): added %s; now have %d msgs", + gameID, DbgUtils.hexDump(msg), msgs.size() ); + } + } + + reportHaveData( gameID, nowHaveData ); + + return msg.length; + } + + private byte[][] getMsgsFor( int gameID ) + { + Assert.assertFalse( gameID == 0 ); + byte[][] result = null; + synchronized ( mMsgMap ) { + if ( mMsgMap.containsKey( gameID ) ) { + List msgs = mMsgMap.get( gameID ); + result = msgs.toArray( new byte[msgs.size()][] ); + } + } + Log.d( TAG, "getMsgsFor() => %d msgs", result == null ? 0 : result.length ); + return result; + } + + private void removeSentMsgs( int gameID, byte[][] msgs ) + { + Boolean nowHaveData = null; + if ( null != msgs ) { + synchronized ( mMsgMap ) { + if ( mMsgMap.containsKey( gameID ) ) { + List list = mMsgMap.get( gameID ); + // Log.d( TAG, "removeSentMsgs(%d): size before: %d", gameID, + // list.size() ); + int origSize = list.size(); + for ( byte[] msg : msgs ) { + list.remove( msg ); + } + if ( 0 < origSize ) { + Log.d( TAG, "removeSentMsgs(%d): size was %d, now %d", gameID, + origSize, list.size() ); + } + nowHaveData = 0 < list.size(); + } + } + } + reportHaveData( gameID, nowHaveData ); + } + + private void reportHaveData( int gameID, Boolean nowHaveData ) + { + Log.d( TAG, "reportHaveData(" + nowHaveData + ")" ); + if ( null != nowHaveData ) { + HaveDataListener proc = null; + synchronized ( mListeners ) { + WeakReference ref = mListeners.get( gameID ); + if ( null != ref ) { + proc = ref.get(); + if ( null == proc ) { + mListeners.remove( gameID ); + } + } else { + Log.d( TAG, "reportHaveData(): no listener for %d", gameID ); + } + } + if ( null != proc ) { + proc.onHaveDataChanged( nowHaveData ); + } + } + } + + static byte[] split( byte[] msg, byte[] headerOut ) + { + headerOut[0] = msg[0]; + byte[] result = Arrays.copyOfRange( msg, 1, msg.length ); + Log.d( TAG, "split(%s) => %d/%s", DbgUtils.hexDump( msg ), + headerOut[0], DbgUtils.hexDump( result ) ); + return result; + } + } + private static MsgsStore sMsgsStore = new MsgsStore(); + + static void setHaveDataListener( int gameID, HaveDataListener listener ) + { + sMsgsStore.setHaveDataListener( gameID, listener ); + } + + static int addMsgFor( byte[] msg, int gameID ) + { + return sMsgsStore.addMsgFor( gameID, MESSAGE, msg ); + } + + static int addInvitationFor( byte[] msg, int gameID ) + { + return sMsgsStore.addMsgFor( gameID, INVITE, msg ); + } + + static int addReplyFor( byte[] msg, int gameID ) + { + return sMsgsStore.addMsgFor( gameID, REPLY, msg ); + } + + static MsgToken getMsgsFor( int gameID ) + { + MsgToken token = new MsgToken( sMsgsStore, gameID ); + return token; + } + + static void receiveMsgs( Context context, byte[] data ) + { + receiveMsgs( context, data, 0 ); + } + + static void receiveMsgs( Context context, byte[] data, int offset ) + { + // Log.d( TAG, "receiveMsgs(gameID=%d, %s, offset=%d)", gameID, + // DbgUtils.hexDump(data), offset ); + DbgUtils.assertOnUIThread( false ); + int[] gameID = {0}; + byte[][] msgs = unformatMsgs( data, offset, gameID ); + if ( null != msgs ) { + NFCServiceHelper helper = new NFCServiceHelper( context ); + for ( byte[] msg : msgs ) { + byte[] typ = {0}; + byte[] body = MsgsStore.split( msg, typ ); + switch ( typ[0] ) { + case MESSAGE: + long[] rowids = DBUtils.getRowIDsFor( context, gameID[0] ); + if ( null == rowids || 0 == rowids.length ) { + addReplyFor( new byte[]{REPLY_NOGAME}, gameID[0] ); + } else { + for ( long rowid : rowids ) { + NFCMsgSink sink = new NFCMsgSink( context, rowid ); + helper.receiveMessage( rowid, sink, body ); + } + } + break; + case INVITE: + GamesListDelegate.postNFCInvite( context, body ); + break; + case REPLY: + switch( body[0] ) { + case REPLY_NOGAME: + // PENDING Don't enable this until deviceID is being + // checked. Otherwise it'll happen every time I tap my + // device against another that doesn't have my game, + // which could be common. + // helper.postEvent( MultiEvent.MESSAGE_NOGAME, gameID ); + Log.e( TAG, "receiveMsgs(): not calling helper.postEvent( " + + "MultiEvent.MESSAGE_NOGAME, gameID );" ); + break; + default: + Log.e( TAG, "unexpected reply %d", body[0] ); + Assert.assertFalse( BuildConfig.DEBUG ); + break; + } + break; + default: + Assert.assertFalse( BuildConfig.DEBUG ); + break; } } } - return success; } private static class NFCServiceHelper extends XWServiceHelper { @@ -248,7 +456,7 @@ public class NFCUtils { private void receiveMessage( long rowid, NFCMsgSink sink, byte[] msg ) { - Log.d( TAG, "receiveMessage()" ); + Log.d( TAG, "receiveMessage(rowid=%d, len=%d)", rowid, msg.length ); receiveMessage( rowid, sink, msg, mAddr ); } } @@ -259,27 +467,4 @@ public class NFCUtils { super( context, rowid ); } } - - private static byte[][] msgsFrom( String json, /*out*/ int[] gameID ) - { - byte[][] result = null; - try { - JSONObject obj = new JSONObject( json ); - gameID[0] = obj.getInt( GAMEID ); - JSONArray arr = obj.getJSONArray( MSGS ); - if ( null != arr ) { - result = new byte[arr.length()][]; - for ( int ii = 0; ii < arr.length(); ++ii ) { - String str = arr.getString( ii ); - result[ii] = Utils.base64Decode( str ); - } - } - } catch ( JSONException ex ) { - Assert.assertFalse( BuildConfig.DEBUG ); - result = null; - } - Log.d( TAG, "msgsFrom() => %s", (Object)result ); - return result; - } - } 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 2a5bd78bc..183d9edce 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 @@ -294,6 +294,7 @@ public class NetLaunchInfo implements Serializable { addP2PInfo( context ); break; case COMMS_CONN_NFC: + addNFCInfo(); break; default: Assert.fail(); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java index db684dd54..f49343e3a 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayService.java @@ -735,9 +735,9 @@ public class RelayService extends XWJIService Log.e( TAG, "fail sending to %s", udpSocket ); Log.ex( TAG, ex ); Log.i( TAG, "Restarting threads to force new socket" ); - ConnStatusHandler.updateStatusOut( service, null, - CommsConnType.COMMS_CONN_RELAY, - true ); + ConnStatusHandler + .updateStatusOut( service, CommsConnType.COMMS_CONN_RELAY, + true ); closeUDPSocket( udpSocket ); service.m_handler.post( new Runnable() { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java index f0a355c51..69fb6c28e 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/Utils.java @@ -615,6 +615,40 @@ public class Utils { return Looper.getMainLooper().equals(Looper.myLooper()); } + // But see hexArray above + private static final String HEX_CHARS = "0123456789ABCDEF"; + private static char[] HEX_CHARS_ARRAY = HEX_CHARS.toCharArray(); + + public static String ba2HexStr( byte[] input ) + { + StringBuffer sb = new StringBuffer(); + + for ( byte byt : input ) { + sb.append(HEX_CHARS_ARRAY[(byt >> 4) & 0x0F]); + sb.append(HEX_CHARS_ARRAY[byt & 0x0F]); + } + + String result = sb.toString(); + return result; + } + + public static byte[] hexStr2ba( String data ) + { + data = data.toUpperCase(); + Assert.assertTrue( 0 == data.length() % 2 ); + byte[] result = new byte[data.length() / 2]; + + for (int ii = 0; ii < data.length(); ii += 2 ) { + int one = HEX_CHARS.indexOf(data.charAt(ii)); + Assert.assertTrue( one >= 0 ); + int two = HEX_CHARS.indexOf(data.charAt(ii + 1)); + Assert.assertTrue( two >= 0 ); + result[ii/2] = (byte)((one << 4) | two); + } + + return result; + } + public static String base64Encode( byte[] in ) { return Base64.encodeToString( in, Base64.NO_WRAP ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/WiDirService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/WiDirService.java index 315f095c5..f795ddfb8 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/WiDirService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/WiDirService.java @@ -172,15 +172,15 @@ public class WiDirService extends XWService { private static void updateStatusOut( boolean success ) { ConnStatusHandler - .updateStatusOut( XWApp.getContext(), null, + .updateStatusOut( XWApp.getContext(), CommsConnType.COMMS_CONN_P2P, success ); } private static void updateStatusIn( boolean success ) { ConnStatusHandler - .updateStatusIn( XWApp.getContext(), null, - CommsConnType.COMMS_CONN_P2P, success ); + .updateStatusIn( XWApp.getContext(), CommsConnType.COMMS_CONN_P2P, + success ); } public static void init( Context context ) 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 d419e496d..8a04996af 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 @@ -80,6 +80,8 @@ public class CommsAddrRec { id = R.string.invite_choice_data_sms; break; case COMMS_CONN_P2P: id = R.string.invite_choice_p2p; break; + case COMMS_CONN_NFC: + id = R.string.invite_choice_nfc; break; default: Assert.assertFalse( BuildConfig.DEBUG ); } 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 daa93c61c..39cbd0426 100644 --- a/xwords4/android/app/src/main/res/values/common_rsrc.xml +++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml @@ -152,7 +152,6 @@ eehouse.org - application/org.eehouse.android.xw4 eehouse.org application/x-xwordsinvite diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index f2a7aafd0..901969f6a 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -2397,5 +2397,7 @@ they\'re committed as moves -- by long-tapping, same as committed words.\n\nUse this feature to check the validity of words you\'re thinking of playing, or to look up an unfamiliar word provided as a - hint. + hint. + + For transmitting CrossWords moves diff --git a/xwords4/android/app/src/main/res/xml/apduservice.xml b/xwords4/android/app/src/main/res/xml/apduservice.xml new file mode 100644 index 000000000..adf18e767 --- /dev/null +++ b/xwords4/android/app/src/main/res/xml/apduservice.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/xwords4/common/comms.c b/xwords4/common/comms.c index bbf0cd805..825184a80 100644 --- a/xwords4/common/comms.c +++ b/xwords4/common/comms.c @@ -2647,26 +2647,27 @@ comms_getStats( CommsCtxt* comms, XWStreamCtxt* stream ) (XP_UCHAR*)"msg queue len: %d\n", comms->queueLen ); stream_catString( stream, buf ); + XP_U16 indx = 0; for ( elem = comms->msgQueueHead; !!elem; elem = elem->next ) { XP_SNPRINTF( buf, sizeof(buf), - " - channelNo=%.4X; msgID=" XP_LD "; len=%d\n", - elem->channelNo, elem->msgID, elem->len ); + "%d: - channelNo=%.4X; msgID=" XP_LD "; len=%d\n", + indx++, elem->channelNo, elem->msgID, elem->len ); stream_catString( stream, buf ); } for ( rec = comms->recs; !!rec; rec = rec->next ) { - XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), - (XP_UCHAR*)" Stats for channel: %.4X\n", + XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), + (XP_UCHAR*)"Stats for channel %.4X\n", rec->channelNo ); stream_catString( stream, buf ); - XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), - (XP_UCHAR*)"Last msg sent: " XP_LD "\n", + XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), + (XP_UCHAR*)" Last msg sent: " XP_LD "; ", rec->nextMsgID ); stream_catString( stream, buf ); - XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), - (XP_UCHAR*)"Last msg received: %d\n", + XP_SNPRINTF( (XP_UCHAR*)buf, sizeof(buf), + (XP_UCHAR*)"last msg received: %d\n", rec->lastMsgRcd ); stream_catString( stream, buf ); } From ae0d2facb47eaaa86ffb02081c2b9a38e13c2635 Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 23 Dec 2019 09:52:56 -0800 Subject: [PATCH 12/24] remove debug option that no longer works (NFC send-to-self would be very different now.) --- .../main/java/org/eehouse/android/xw4/BoardDelegate.java | 5 +---- .../java/org/eehouse/android/xw4/InviteChoicesAlert.java | 2 +- .../src/main/java/org/eehouse/android/xw4/XWPrefs.java | 5 ----- xwords4/android/app/src/main/res/values/common_rsrc.xml | 1 - xwords4/android/app/src/main/res/values/strings.xml | 2 -- xwords4/android/app/src/main/res/xml/xwprefs.xml | 6 ------ xwords4/android/res_src/values-ba_CK/strings.xml | 2 -- xwords4/android/res_src/values-ca/strings.xml | 4 +--- xwords4/android/res_src/values-ca_PS/strings.xml | 2 -- xwords4/android/res_src/values-de/strings.xml | 4 +--- xwords4/android/res_src/values-fr/strings.xml | 9 +-------- xwords4/android/res_src/values-ja/strings.xml | 4 +--- xwords4/android/res_src/values-nb-rNO/strings.xml | 4 +--- xwords4/android/res_src/values-nl/strings.xml | 2 -- xwords4/android/res_src/values-pl/strings.xml | 2 -- 15 files changed, 7 insertions(+), 47 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java index a0a0bc594..97349641c 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java @@ -1279,10 +1279,7 @@ public class BoardDelegate extends DelegateBase ? (SentInvitesInfo)params[0] : null; switch( means ) { case NFC: - if ( XWPrefs.getNFCToSelfEnabled( m_activity ) ) { - makeConfirmThenBuilder( R.string.nfc_to_self, Action.NFC_TO_SELF ) - .show(); - } else if ( ! NFCUtils.nfcAvail( m_activity )[1] ) { + if ( ! NFCUtils.nfcAvail( m_activity )[1] ) { showDialogFragment( DlgID.ENABLE_NFC ); } else { makeOkOnlyBuilder( R.string.nfc_just_tap ).show(); 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 b3f13360c..9da3c9c58 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 @@ -79,7 +79,7 @@ public class InviteChoicesAlert extends DlgDelegateAlert { if ( WiDirWrapper.enabled() ) { add( items, means, R.string.invite_choice_p2p, InviteMeans.WIFIDIRECT ); } - if ( XWPrefs.getNFCToSelfEnabled( context ) || NFCUtils.nfcAvail( context )[0] ) { + if ( NFCUtils.nfcAvail( context )[0] ) { add( items, means, R.string.invite_choice_nfc, InviteMeans.NFC ); } add( items, means, R.string.slmenu_copy_sel, InviteMeans.CLIPBOARD ); 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 22cdd8972..bcd2ea7cb 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 @@ -61,11 +61,6 @@ public class XWPrefs { return getPrefsBoolean( context, R.string.key_enable_dup_invite, false ); } - public static boolean getNFCToSelfEnabled( Context context ) - { - return getPrefsBoolean( context, R.string.key_enable_nfc_toself, false ); - } - public static boolean getIgnoreFCM( Context context ) { String curValue = 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 39cbd0426..dce097a73 100644 --- a/xwords4/android/app/src/main/res/values/common_rsrc.xml +++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml @@ -126,7 +126,6 @@ key_na_dicts key_enable_debug key_enable_dup_invite - key_enable_nfc_toself key_enable_sms_toself key_show_fcm2 key_nag_intervals diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 901969f6a..986e328c0 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -2198,8 +2198,6 @@ Fake locale for translation Accept invitations more than once Reminder intervals (minutes1,minutes2,…) - Enable NFC to self - Fake invitation to aid debugging Short-circuit SMS to self Skip radio when phone numbers same Pretend to have radio diff --git a/xwords4/android/app/src/main/res/xml/xwprefs.xml b/xwords4/android/app/src/main/res/xml/xwprefs.xml index c911ca6fc..9d72915af 100644 --- a/xwords4/android/app/src/main/res/xml/xwprefs.xml +++ b/xwords4/android/app/src/main/res/xml/xwprefs.xml @@ -390,12 +390,6 @@ android:defaultValue="false" /> - - Ekaf elacol rof noitalsnart Tpecca snoitativni erom naht ecno Rednimer slavretni ,2setunim,1setunim(...) - Elbane CFN ot fles - Ekaf noitativni ot dia gniggubed Tiucric-trohs SMS ot fles Piks oidar nehw enohp srebmun emas Dneterp ot evah oidar diff --git a/xwords4/android/res_src/values-ca/strings.xml b/xwords4/android/res_src/values-ca/strings.xml index 681c857bc..246a4cfd5 100644 --- a/xwords4/android/res_src/values-ca/strings.xml +++ b/xwords4/android/res_src/values-ca/strings.xml @@ -722,8 +722,6 @@ Introduïu el vostre nom aquí. S\'usarà en crear partides noves. (Podreu Estadístiques de la partida en xarxa ID de la versió de codi font Port de l\'aparell repetidor - Habilita NFC a un mateix - Simula la invitació per a ajudar a la depuració SMS curtcircuit a un mateix Simula tenir ràdio No ho simulis @@ -980,4 +978,4 @@ aparells seleccionats? SMS de dades Encara no s\'ha detectat cap aparell aparellat. Mou a l\'arxiu - \ No newline at end of file + diff --git a/xwords4/android/res_src/values-ca_PS/strings.xml b/xwords4/android/res_src/values-ca_PS/strings.xml index 8f0340ab4..29a491b26 100644 --- a/xwords4/android/res_src/values-ca_PS/strings.xml +++ b/xwords4/android/res_src/values-ca_PS/strings.xml @@ -2204,8 +2204,6 @@ FAKE LOCALE FOR TRANSLATION ACCEPT INVITATIONS MORE THAN ONCE REMINDER INTERVALS (MINUTES1,MINUTES2,...) - ENABLE NFC TO SELF - FAKE INVITATION TO AID DEBUGGING SHORT-CIRCUIT SMS TO SELF SKIP RADIO WHEN PHONE NUMBERS SAME PRETEND TO HAVE RADIO diff --git a/xwords4/android/res_src/values-de/strings.xml b/xwords4/android/res_src/values-de/strings.xml index 81c6cf210..8564b2800 100644 --- a/xwords4/android/res_src/values-de/strings.xml +++ b/xwords4/android/res_src/values-de/strings.xml @@ -740,7 +740,6 @@ Git-Informationen in die Zwischenablage kopieren Einladungen mehrfach annehmen Erinnerungsintervall (Minuten1, Minuten2, …) - Pseudo-Einladung zur Fehlersuche GSM CDMA Standard-Wortliste für Sprache holen @@ -881,7 +880,6 @@ Partien auf SD-Kartie speichern Doppelte Einladungen annehmen Pseudo-Locale zum Übersetzen - NFC-Selbstverbindung aktivieren SMS-Selbstverbindung aktivieren Mobilfunk weglassen bei gleichen Nummern Mobilfunk vortäuschen @@ -1005,4 +1003,4 @@ Sie können Wörter nachschlagen, BEVOR sie als Züge übertragen werden - durch langes Antippen, genau wie bei gebundenen Wörtern. \n \nVerwenden Sie diese Funktion, um die Gültigkeit von Wörtern zu überprüfen, die Sie spielen möchten, oder um ein unbekanntes Wort als Hinweis nachzuschlagen. - \ No newline at end of file + diff --git a/xwords4/android/res_src/values-fr/strings.xml b/xwords4/android/res_src/values-fr/strings.xml index 932297501..7eb7ff73c 100644 --- a/xwords4/android/res_src/values-fr/strings.xml +++ b/xwords4/android/res_src/values-fr/strings.xml @@ -2924,13 +2924,6 @@ les release builds) Intervalles des rappels (minutes1,minutes2,…) - - - Activer la NFC à soi-même - - - Fausse invitation pour aider le -débuggage Court-circuiter les SMS vers soi-même @@ -3223,4 +3216,4 @@ Vous pouvez la ré-ouvrir pour que la permission soit redemandée. Ou vous pouve Cette notification est présente dès que CrossWords tourne en tâche de fond pour recevoir des messages Bluetooth. Elle reste pendant environ 15 minutes après que CrossWords ait démarré ou qu\'un message Bluetooth ait été reçu. Cette nouvelle option lance l\'appli SMS par défaut avec une invitation prête à l\'envoie… si elle marche. Les applis SMS sont toutes différentes : certaine sont réticentes. Impossible de lancer l\'appli SMS - \ No newline at end of file + diff --git a/xwords4/android/res_src/values-ja/strings.xml b/xwords4/android/res_src/values-ja/strings.xml index 0101da473..40e14fde2 100644 --- a/xwords4/android/res_src/values-ja/strings.xml +++ b/xwords4/android/res_src/values-ja/strings.xml @@ -727,8 +727,6 @@ 翻訳の擬似地域 2 回以上の招待を受け入れます リマインダーの間隔 (分1、分2、...) - 自分への NFC を有効にする - デバッグを支援するための擬似招待状 自分に SMS を短絡させる 電話番号が同じときに無線をスキップします 無線があるとみなす @@ -1006,4 +1004,4 @@ WiFi ダイレクト経由で接続可能なデバイスはありません。単語を移動としてコミットする前に -- 長押しして、コミットされた単語と同じように単語を調べることができます。 \n \nこの機能を使用して、考えている単語の有効性を確認したり、ヒントとして提供される見慣れない単語を調べたりします。 - \ No newline at end of file + diff --git a/xwords4/android/res_src/values-nb-rNO/strings.xml b/xwords4/android/res_src/values-nb-rNO/strings.xml index 729a51b44..732f98dd4 100644 --- a/xwords4/android/res_src/values-nb-rNO/strings.xml +++ b/xwords4/android/res_src/values-nb-rNO/strings.xml @@ -681,8 +681,6 @@ Godta duplikatinvitasjoner Juks til lokale for oversettelse Påminnelsesintervall (minutt1,minutt2,…) - Skru på NFC til egen enhet - Juks til invitasjon for å hjelpe i feilrettingsøyemed Bruk ny/eksperimentell SMS-kode (Krever at motparten bruker det også) Send via NFC til egen ehet? @@ -1002,4 +1000,4 @@ Kun prøv: Ingen FCM-meldinger Kun FCM: Ingen prøving Forvalg: Miks prøving og FCM - \ No newline at end of file + diff --git a/xwords4/android/res_src/values-nl/strings.xml b/xwords4/android/res_src/values-nl/strings.xml index e47373f8c..30c3a3820 100644 --- a/xwords4/android/res_src/values-nl/strings.xml +++ b/xwords4/android/res_src/values-nl/strings.xml @@ -729,8 +729,6 @@ Neppe taal voor vertalingen Accepteer uitnodigingen meer dan eens Herinnering intervallen (minuten1,minuten2,...) - Schakel NFC naar zelf in - Nepuitnodiging om debuggen te ondersteunen Stuur SMS berichten naar zelf Schakel radio over als telefoonnummers gelijk zijn Doe net alsof er een radio is diff --git a/xwords4/android/res_src/values-pl/strings.xml b/xwords4/android/res_src/values-pl/strings.xml index a1f00e7ff..b5394f6bd 100644 --- a/xwords4/android/res_src/values-pl/strings.xml +++ b/xwords4/android/res_src/values-pl/strings.xml @@ -784,8 +784,6 @@ Fałszywy język do tłumaczenia Akceptuj zaproszenia więcej niż raz Przypomnienie w odstępach (1 minuty, 2 minut,...) - Włącz NFC dla siebie (samoczynnie\?) - Fałszywe zaproszenie do pomocy w debugowaniu Akceptuj SMS do siebie GSM CDMA From 85fd353829afee68a4829acbc48dd778e0dc2e92 Mon Sep 17 00:00:00 2001 From: Eric House Date: Thu, 26 Dec 2019 09:13:59 -0800 Subject: [PATCH 13/24] initialize variable flagged by valgrind --- xwords4/common/boarddrw.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xwords4/common/boarddrw.c b/xwords4/common/boarddrw.c index 7b732e54d..7010dfd1b 100644 --- a/xwords4/common/boarddrw.c +++ b/xwords4/common/boarddrw.c @@ -373,7 +373,7 @@ drawCell( BoardCtxt* board, const XP_U16 col, const XP_U16 row, XP_Bool skipBlan XP_Bool success = XP_TRUE; XP_Rect cellRect = {0}; Tile tile; - XP_Bool isBlank, isEmpty, recent, pending = XP_FALSE; + XP_Bool isBlank, isEmpty, recent = XP_FALSE, pending = XP_FALSE; XWBonusType bonus; ModelCtxt* model = board->model; DictionaryCtxt* dict = model_getDictionary( model ); From 29480d2867c35cd09a05d70eb0d23abe629545cc Mon Sep 17 00:00:00 2001 From: Eric House Date: Thu, 26 Dec 2019 09:03:33 -0800 Subject: [PATCH 14/24] use the right variable for array access --- xwords4/common/board.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/xwords4/common/board.c b/xwords4/common/board.c index f5cfb3ec8..89ba5d3d5 100644 --- a/xwords4/common/board.c +++ b/xwords4/common/board.c @@ -2382,8 +2382,7 @@ XP_Bool coordToCell( const BoardCtxt* board, XP_S16 xx, XP_S16 yy, XP_U16* colP, XP_U16* rowP ) { - XP_U16 col, row; - XP_U16 maxCols = model_numCols( board->model ); + const XP_U16 maxCols = model_numCols( board->model ); XP_S16 gotCol = -1; XP_S16 gotRow = -1; const ScrollData* hsd = &board->sd[SCROLL_H]; @@ -2394,7 +2393,7 @@ coordToCell( const BoardCtxt* board, XP_S16 xx, XP_S16 yy, XP_U16* colP, if ( xx >= 0 && yy >= 0 ) { - for ( col = hsd->offset; col < maxCols; ++col ) { + for ( XP_U16 col = hsd->offset; col < maxCols; ++col ) { xx -= hsd->dims[col]; if ( xx <= 0 ) { gotCol = col; @@ -2402,8 +2401,8 @@ coordToCell( const BoardCtxt* board, XP_S16 xx, XP_S16 yy, XP_U16* colP, } } - for ( row = vsd->offset; row < maxCols; ++row ) { - yy -= vsd->dims[col]; + for ( XP_U16 row = vsd->offset; row < maxCols; ++row ) { + yy -= vsd->dims[row]; if ( yy <= 0 ) { gotRow = row; break; From 8b06cde971761c3b2d8c6a38598da60ccac848f8 Mon Sep 17 00:00:00 2001 From: Eric House Date: Fri, 27 Dec 2019 15:44:23 -0800 Subject: [PATCH 15/24] up strings for new release and cleanup --- xwords4/android/app/build.gradle | 6 +-- .../android/app/src/main/assets/changes.html | 22 ++++---- .../org/eehouse/android/xw4/BTService.java | 2 +- .../eehouse/android/xw4/BoardDelegate.java | 4 -- .../org/eehouse/android/xw4/DlgDelegate.java | 1 - .../android/xw4/GamesListDelegate.java | 1 - .../org/eehouse/android/xw4/MultiMsgSink.java | 2 - .../eehouse/android/xw4/NFCCardService.java | 52 ++++++++----------- .../org/eehouse/android/xw4/NFCUtils.java | 2 +- xwords4/android/jni/xportwrapper.c | 1 - xwords4/common/board.c | 5 +- 11 files changed, 41 insertions(+), 57 deletions(-) diff --git a/xwords4/android/app/build.gradle b/xwords4/android/app/build.gradle index 0e701a83f..32c3a4842 100644 --- a/xwords4/android/app/build.gradle +++ b/xwords4/android/app/build.gradle @@ -1,10 +1,10 @@ def INITIAL_CLIENT_VERS = 9 -def VERSION_CODE_BASE = 146 -def VERSION_NAME = '4.4.150' +def VERSION_CODE_BASE = 147 +def VERSION_NAME = '4.4.151' def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY") def BUILD_INFO_NAME = "build-info.txt" -// AID must start with F (first 4 bits) and be at from 5 to 16 bytes long +// AID must start with F (first 4 bits) and be from 5 to 16 bytes long def NFC_AID_XW4 = "FC8FF510B360" def NFC_AID_XW4d = "FDDA0A3EB5E5" diff --git a/xwords4/android/app/src/main/assets/changes.html b/xwords4/android/app/src/main/assets/changes.html index ab832c830..2d286cf3b 100644 --- a/xwords4/android/app/src/main/assets/changes.html +++ b/xwords4/android/app/src/main/assets/changes.html @@ -13,11 +13,11 @@ -

    CrossWords 4.4.150 release

    +

    CrossWords 4.4.151 release

    -

    This release speeds move delivery for newly-installed games and - hides invite-via-NFC on Android 10 (since it doesn't work - there).

    +

    This release fixes a bad crash, adds a lot of translations in + several languages, and lets you invite and exchange moves via + NFC on any Android version.

    Please take @@ -27,11 +27,13 @@

    New with this release

      -
    • Improve message delivery for new installs
    • -
    • Don't offer NFC on Android 10, where it's broken
    • -
    • Improve board layout on taller, narrower screens
    • -
    • Fix occasional stall sending chat messages
    • -
    • Fix another (very rare!) type of stall
    • +
    • Fix crash dragging tiles
    • +
    • Fix invite-via-NFC for Android 10 (and improve for earlier + Android versions)
    • +
    • Add ability to send moves via NFC
    • +
    • More translations (via Weblate) in Catalan, French, German, + Japanese, Norwegian, Polish, and Spanish
    • +
    • Fix a couple of other crashes reported via Google (thanks!)

    (The full changelog @@ -39,7 +41,7 @@

    Next up

      -
    • Fix email invitations
    • +
    • Improve move-via-NFC
    • Support duplicate-style play (popular in France)
    • Improve play-by-data-sms workaround using NBSProxy
    • diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java index 4bc093d54..dbb8cda93 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTService.java @@ -689,7 +689,7 @@ public class BTService extends XWJIService { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); if ( null != adapter ) { for ( BluetoothDevice dev : adapter.getBondedDevices() ) { - Log.d( TAG, "%s => %s", dev.getName(), dev.getAddress() ); + // Log.d( TAG, "%s => %s", dev.getName(), dev.getAddress() ); if ( btName.equals( dev.getName() ) ) { btAddr = dev.getAddress(); s_namesToAddrs.put( btName, btAddr ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java index 97349641c..994029ab0 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java @@ -1102,10 +1102,6 @@ public class BoardDelegate extends DelegateBase case LOOKUP_ACTION: launchLookup( m_mySIS.words, m_gi.dictLang ); break; - case NFC_TO_SELF: - Assert.assertFalse( BuildConfig.DEBUG ); - // GamesListDelegate.sendNFCToSelf( m_activity, makeNFCMessage() ); - break; case DROP_RELAY_ACTION: dropConViaAndRestart(CommsConnType.COMMS_CONN_RELAY); break; 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 35299e55d..80014cf7f 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 @@ -76,7 +76,6 @@ public class DlgDelegate { VALUES_ACTION, SMS_CONFIG_ACTION, BUTTON_BROWSEALL_ACTION, - NFC_TO_SELF, DROP_RELAY_ACTION, DROP_SMS_ACTION, INVITE_SMS_DATA, 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 ba91df632..01882a574 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 @@ -64,7 +64,6 @@ import org.eehouse.android.xw4.loc.LocUtils; import java.io.Serializable; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.Iterator; 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 999e03c74..e72d5bafe 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 @@ -81,7 +81,6 @@ public class MultiMsgSink implements TransportProcs { int sendViaNFC( byte[] buf, int gameID ) { - Log.d( TAG, "sendViaNFC(gameID=%d, len=%d)", gameID, buf.length ); return NFCUtils.addMsgFor( buf, gameID ); } @@ -113,7 +112,6 @@ public class MultiMsgSink implements TransportProcs { nSent = sendViaP2P( buf, gameID, addr ); break; case COMMS_CONN_NFC: - Log.d( TAG, "transportSend(): got for NFC" ); nSent = sendViaNFC( buf, gameID ); break; default: diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java index f87d624df..e5c8f47b1 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java @@ -241,11 +241,9 @@ public class NFCCardService extends HostApduService { if ( null != apdu ) { if ( HEX_STR.CMD_MSG_PART.matchesFrom( apdu ) ) { resStr = HEX_STR.STATUS_SUCCESS; - int[] msgID = {0}; - byte[] all = reassemble( this, apdu, msgID, HEX_STR.CMD_MSG_PART ); + byte[] all = reassemble( this, apdu, HEX_STR.CMD_MSG_PART ); if ( null != all ) { addToMsgThread( this, all ); - setLatestAck( msgID[0] ); } } else { Log.d( TAG, "processCommandApdu(): aid case?" ); @@ -379,20 +377,19 @@ public class NFCCardService extends HostApduService { private static byte[][] sParts = null; private static int sMsgID = 0; private synchronized static byte[] reassemble( Context context, byte[] part, - int[] msgIDOut, HEX_STR cmd ) + HEX_STR cmd ) { - return reassemble( context, part, msgIDOut, cmd.length() ); + return reassemble( context, part, cmd.length() ); } private synchronized static byte[] reassemble( Context context, byte[] part, - int[] msgIDOut, int offset ) + int offset ) { part = Arrays.copyOfRange( part, offset, part.length ); - return reassemble( context, part, msgIDOut ); + return reassemble( context, part ); } - private synchronized static byte[] reassemble( Context context, - byte[] part, int[] msgIDOut ) + private synchronized static byte[] reassemble( Context context, byte[] part ) { byte[] result = null; try { @@ -438,10 +435,10 @@ public class NFCCardService extends HostApduService { sParts = null; result = baos.toByteArray(); - msgIDOut[0] = sMsgID; + setLatestAck( sMsgID ); if ( 0 != sMsgID ) { Log.d( TAG, "reassemble(): done reassembling msgID=%d: %s", - msgIDOut[0], DbgUtils.hexDump(result) ); + sMsgID, DbgUtils.hexDump(result) ); } } } @@ -495,7 +492,8 @@ public class NFCCardService extends HostApduService { int latestAck = getLatestAck(); baos.write( numTo( latestAck ) ); } - Assert.assertTrue( HEADER_SIZE >= baos.toByteArray().length ); + Assert.assertTrue( HEADER_SIZE >= baos.toByteArray().length + || !BuildConfig.DEBUG ); int thisLen = Math.min( maxLen - HEADER_SIZE, length - offset ); if ( 0 < thisLen ) { @@ -533,8 +531,9 @@ public class NFCCardService extends HostApduService { public static Wrapper init( Activity activity, Procs procs, int devID ) { Wrapper instance = null; - if ( null != NfcAdapter.getDefaultAdapter( activity ) ) { - instance = new Wrapper( activity, procs, devID ); + NfcAdapter adapter = NfcAdapter.getDefaultAdapter( activity ); + if ( null != adapter ) { + instance = new Wrapper( activity, adapter, procs, devID ); } Log.d( TAG, "Wrapper.init(devID=%d) => %s", devID, instance ); return instance; @@ -554,12 +553,13 @@ public class NFCCardService extends HostApduService { } } - private Wrapper( Activity activity, Procs procs, int devID ) + private Wrapper( Activity activity, NfcAdapter adapter, Procs procs, + int devID ) { mActivity = activity; + mAdapter = adapter; mProcs = procs; mMyDevID = devID; - mAdapter = NfcAdapter.getDefaultAdapter( activity ); } private void setResumed( boolean resumed ) @@ -681,16 +681,18 @@ public class NFCCardService extends HostApduService { if ( statusOK ) { int offset = HEX_STR.STATUS_SUCCESS.length(); if ( HEX_STR.CMD_MSG_PART.matchesFrom( response, offset ) ) { - int[] msgID = {0}; - byte[] all = reassemble( mActivity, response, msgID, + byte[] all = reassemble( mActivity, response, offset + HEX_STR.CMD_MSG_PART.length() ); + Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); if ( null != all ) { addToMsgThread( mActivity, all ); - setLatestAck( msgID[0] ); + } } } - Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); + if ( !statusOK ) { + Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); + } return statusOK; } @@ -729,16 +731,6 @@ public class NFCCardService extends HostApduService { } catch ( InterruptedException ie ) { Log.d( TAG, "run interrupted" ); } - // toggle(); - // try { - // // How long to sleep. - // int intervalMS = mMinMS + (Math.abs(mRandom.nextInt()) - // % (mMaxMS - mMinMS)); - // // Log.d( TAG, "sleeping for %d ms", intervalMS ); - // Thread.sleep( intervalMS ); - // } catch ( InterruptedException ie ) { - // Log.d( TAG, "run interrupted" ); - // } } // Kill read mode on the way out 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 56772b53a..76a7bbd98 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 @@ -89,7 +89,7 @@ public class NFCUtils { result = intent.getByteArrayExtra( NFC_TO_SELF_DATA ); } - Log.d( TAG, "getFromIntent() => %s", result ); + // Log.d( TAG, "getFromIntent() => %s", result ); return result; } diff --git a/xwords4/android/jni/xportwrapper.c b/xwords4/android/jni/xportwrapper.c index f3c8597d3..ca5ed0b65 100644 --- a/xwords4/android/jni/xportwrapper.c +++ b/xwords4/android/jni/xportwrapper.c @@ -148,7 +148,6 @@ and_xport_sendNoConn( const XP_U8* buf, XP_U16 len, const XP_UCHAR* msgNo, static void and_xport_countChanged( void* closure, XP_U16 count ) { - XP_LOGF( "%s(count=%d)", __func__, count ); AndTransportProcs* aprocs = (AndTransportProcs*)closure; if ( NULL != aprocs && NULL != aprocs->jxport ) { JNIEnv* env = ENVFORME( aprocs->ti ); diff --git a/xwords4/common/board.c b/xwords4/common/board.c index 89ba5d3d5..6aab7422c 100644 --- a/xwords4/common/board.c +++ b/xwords4/common/board.c @@ -2385,14 +2385,12 @@ coordToCell( const BoardCtxt* board, XP_S16 xx, XP_S16 yy, XP_U16* colP, const XP_U16 maxCols = model_numCols( board->model ); XP_S16 gotCol = -1; XP_S16 gotRow = -1; - const ScrollData* hsd = &board->sd[SCROLL_H]; - const ScrollData* vsd = &board->sd[SCROLL_V]; xx -= board->boardBounds.left; yy -= board->boardBounds.top; if ( xx >= 0 && yy >= 0 ) { - + const ScrollData* hsd = &board->sd[SCROLL_H]; for ( XP_U16 col = hsd->offset; col < maxCols; ++col ) { xx -= hsd->dims[col]; if ( xx <= 0 ) { @@ -2401,6 +2399,7 @@ coordToCell( const BoardCtxt* board, XP_S16 xx, XP_S16 yy, XP_U16* colP, } } + const ScrollData* vsd = &board->sd[SCROLL_V]; for ( XP_U16 row = vsd->offset; row < maxCols; ++row ) { yy -= vsd->dims[row]; if ( yy <= 0 ) { From d4d4693defc486e8c76176a33eaa859df90c687d Mon Sep 17 00:00:00 2001 From: Eric House Date: Sat, 28 Dec 2019 09:01:10 -0800 Subject: [PATCH 16/24] makefile for new French wordlist --- xwords4/dawg/French/Makefile.ODS8 | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 xwords4/dawg/French/Makefile.ODS8 diff --git a/xwords4/dawg/French/Makefile.ODS8 b/xwords4/dawg/French/Makefile.ODS8 new file mode 100644 index 000000000..4348f1e18 --- /dev/null +++ b/xwords4/dawg/French/Makefile.ODS8 @@ -0,0 +1,36 @@ +# -*-mode: Makefile -*- +# Copyright 2016 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. + +XWLANG=ODS8 +LANGCODE=fr_FR +DICTNOTE = "From ods8.zip submitted by a user" + +TARGET_TYPE ?= WINCE + +include ../Makefile.langcommon + +# use sed to strip the bogus utf8 identifier. There must be a better +# way, e.g. with iconv. +$(XWLANG)Main.dict.gz: $(XWDICTPATH)/French/ods8.txt + cat $< | tr -d '\r' | sed 's,\xEF\xBB\xBF,,' | tr a-z A-Z | gzip >$@ + +# Everything but creating of the Main.dict file is inherited from the +# "parent" Makefile.langcommon in the parent directory. + +clean: clean_common + rm -f $(XWLANG)Main.dict.gz *.bin $(XWLANG)*.pdb $(XWLANG)*.seb From 42da9b1ebf9c2152d24d9835cce539e19817a678 Mon Sep 17 00:00:00 2001 From: Eric House Date: Sat, 28 Dec 2019 09:43:01 -0800 Subject: [PATCH 17/24] don't allow devid of 0 --- .../app/src/main/java/org/eehouse/android/xw4/DevID.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java index ac36e8936..6d291112b 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java @@ -154,7 +154,7 @@ public class DevID { synchronized ( sNFCDevID ) { if ( 0 == sNFCDevID[0] ) { int devid = DBUtils.getIntFor( context, NFC_DEVID_KEY, 0 ); - if ( 0 == devid ) { + while ( 0 == devid ) { devid = Utils.nextRandomInt(); DBUtils.setIntFor( context, NFC_DEVID_KEY, devid ); } From 381efc9ddbab94ba6d9c48efdea787c994d206ae Mon Sep 17 00:00:00 2001 From: Eric House Date: Sun, 29 Dec 2019 06:45:41 -0800 Subject: [PATCH 18/24] refactor to avoid ClassNotFound crash where SDK<19 --- .../eehouse/android/xw4/BoardDelegate.java | 2 +- .../eehouse/android/xw4/NFCCardService.java | 648 +----------------- .../org/eehouse/android/xw4/NFCUtils.java | 632 ++++++++++++++++- 3 files changed, 641 insertions(+), 641 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java index 994029ab0..025567323 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BoardDelegate.java @@ -72,7 +72,7 @@ import org.eehouse.android.xw4.jni.XwJNI.GamePtr; import org.eehouse.android.xw4.jni.XwJNI; import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.TilePickAlert.TilePickState; -import org.eehouse.android.xw4.NFCCardService.Wrapper; +import org.eehouse.android.xw4.NFCUtils.Wrapper; public class BoardDelegate extends DelegateBase implements TransportProcs.TPMsgHandler, View.OnClickListener, diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java index e5c8f47b1..393ca9cba 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCCardService.java @@ -21,204 +21,22 @@ package org.eehouse.android.xw4; import android.app.Activity; import android.content.Context; -import android.nfc.NfcAdapter; -import android.nfc.Tag; import android.nfc.cardemulation.HostApduService; -import android.nfc.tech.IsoDep; import android.os.Bundle; -import android.text.TextUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.IOException; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicInteger; +import org.eehouse.android.xw4.NFCUtils.HEX_STR; import org.eehouse.android.xw4.NFCUtils.MsgToken; -import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; public class NFCCardService extends HostApduService { private static final String TAG = NFCCardService.class.getSimpleName(); - private static final boolean USE_BIGINTEGER = true; private static final int LEN_OFFSET = 4; - private static final byte VERSION_1 = (byte)0x01; private int mMyDevID; - private static enum HEX_STR { - DEFAULT_CLA( "00" ) - , SELECT_INS( "A4" ) - , STATUS_FAILED( "6F00" ) - , CLA_NOT_SUPPORTED( "6E00" ) - , INS_NOT_SUPPORTED( "6D00" ) - , STATUS_SUCCESS( "9000" ) - , CMD_MSG_PART( "70FC" ) - ; - - private byte[] mBytes; - private HEX_STR( String hex ) { mBytes = Utils.hexStr2ba(hex); } - private byte[] asBA() { return mBytes; } - private boolean matchesFrom( byte[] src ) - { - return matchesFrom( src, 0 ); - } - private boolean matchesFrom( byte[] src, int offset ) - { - boolean result = offset + mBytes.length <= src.length; - for ( int ii = 0; result && ii < mBytes.length; ++ii ) { - result = src[offset + ii] == mBytes[ii]; - } - // Log.d( TAG, "%s.matchesFrom(%s) => %b", this, src, result ); - return result; - } - int length() { return asBA().length; } - } - - private static int sNextMsgID = 0; - private static synchronized int getNextMsgID() - { - return ++sNextMsgID; - } - - private static byte[] numTo( int num ) - { - byte[] result; - if ( USE_BIGINTEGER ) { - BigInteger bi = BigInteger.valueOf( num ); - byte[] bibytes = bi.toByteArray(); - result = new byte[1 + bibytes.length]; - result[0] = (byte)bibytes.length; - System.arraycopy( bibytes, 0, result, 1, bibytes.length ); - } else { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DataOutputStream dos = new DataOutputStream( baos ); - try { - dos.writeInt( num ); - dos.flush(); - } catch ( IOException ioe ) { - Assert.assertFalse( BuildConfig.DEBUG ); - } - result = baos.toByteArray(); - } - // Log.d( TAG, "numTo(%d) => %s", num, DbgUtils.hexDump(result) ); - return result; - } - - private static int numFrom( ByteArrayInputStream bais ) throws IOException - { - int biLen = bais.read(); - // Log.d( TAG, "numFrom(): read biLen: %d", biLen ); - byte[] bytes = new byte[biLen]; - bais.read( bytes ); - BigInteger bi = new BigInteger( bytes ); - int result = bi.intValue(); - - // Log.d( TAG, "numFrom() => %d", result ); - return result; - } - - private static int numFrom( byte[] bytes, int start, int out[] ) - { - int result; - if ( USE_BIGINTEGER ) { - byte biLen = bytes[start]; - byte[] rest = Arrays.copyOfRange( bytes, start + 1, start + 1 + biLen ); - BigInteger bi = new BigInteger(rest); - out[0] = bi.intValue(); - result = biLen + 1; - } else { - ByteArrayInputStream bais = new ByteArrayInputStream( bytes, start, - bytes.length - start ); - DataInputStream dis = new DataInputStream( bais ); - try { - out[0] = dis.readInt(); - } catch ( IOException ioe ) { - Log.e( TAG, "from readInt(): %s", ioe.getMessage() ); - } - result = bais.available() - start; - } - return result; - } - - private static void testNumThing() - { - Log.d( TAG, "testNumThing() starting" ); - - int[] out = {0}; - for ( int ii = 1; ii > 0 && ii < Integer.MAX_VALUE; ii *= 2 ) { - byte[] tmp = numTo( ii ); - numFrom( tmp, 0, out ); - if ( ii != out[0] ) { - Log.d( TAG, "testNumThing(): %d failed; got %d", ii, out[0] ); - break; - } else { - Log.d( TAG, "testNumThing(): %d ok", ii ); - } - } - Log.d( TAG, "testNumThing() DONE" ); - } - - private static class QueueElem { - Context context; - byte[] msg; - QueueElem( Context pContext, byte[] pMsg ) { - context = pContext; - msg = pMsg; - } - } - - private static LinkedBlockingQueue sQueue = null; - - private synchronized static void addToMsgThread( Context context, byte[] msg ) - { - if ( 0 < msg.length ) { - QueueElem elem = new QueueElem( context, msg ); - if ( null == sQueue ) { - sQueue = new LinkedBlockingQueue<>(); - new Thread( new Runnable() { - @Override - public void run() { - Log.d( TAG, "addToMsgThread(): run starting" ); - for ( ; ; ) { - try { - QueueElem elem = sQueue.take(); - NFCUtils.receiveMsgs( elem.context, elem.msg ); - updateStatus( elem.context, true ); - } catch ( InterruptedException ie ) { - break; - } - } - Log.d( TAG, "addToMsgThread(): run exiting" ); - } - } ).start(); - } - sQueue.add( elem ); - // } else { - // // This is very common right now - // Log.d( TAG, "addToMsgThread(): dropping 0-length msg" ); - } - } - - private static void updateStatus( Context context, boolean in ) - { - if ( in ) { - ConnStatusHandler - .updateStatusIn( context, CommsConnType.COMMS_CONN_NFC, true ); - } else { - ConnStatusHandler - .updateStatusOut( context, CommsConnType.COMMS_CONN_NFC, true ); - } - } - // Remove this once we don't need logging to confirm stuff's loading @Override public void onCreate() @@ -241,9 +59,9 @@ public class NFCCardService extends HostApduService { if ( null != apdu ) { if ( HEX_STR.CMD_MSG_PART.matchesFrom( apdu ) ) { resStr = HEX_STR.STATUS_SUCCESS; - byte[] all = reassemble( this, apdu, HEX_STR.CMD_MSG_PART ); + byte[] all = NFCUtils.reassemble( this, apdu, HEX_STR.CMD_MSG_PART ); if ( null != all ) { - addToMsgThread( this, all ); + NFCUtils.addToMsgThread( this, all ); } } else { Log.d( TAG, "processCommandApdu(): aid case?" ); @@ -268,11 +86,11 @@ public class NFCCardService extends HostApduService { if ( BuildConfig.NFC_AID.equals( aidStr ) ) { byte minVersion = (byte)bais.read(); byte maxVersion = (byte)bais.read(); - if ( minVersion == VERSION_1 ) { - int devID = numFrom( bais ); + if ( minVersion == NFCUtils.VERSION_1 ) { + int devID = NFCUtils.numFrom( bais ); Log.d( TAG, "processCommandApdu(): read " + "remote devID: %d", devID ); - mGameID = numFrom( bais ); + mGameID = NFCUtils.numFrom( bais ); Log.d( TAG, "read gameID: %d", mGameID ); if ( 0 < bais.available() ) { Log.d( TAG, "processCommandApdu(): " @@ -301,11 +119,11 @@ public class NFCCardService extends HostApduService { baos.write( resStr.asBA() ); if ( HEX_STR.STATUS_SUCCESS == resStr ) { if ( isAidCase ) { - baos.write( VERSION_1 ); // min - baos.write( numTo( mMyDevID ) ); + baos.write( NFCUtils.VERSION_1 ); // min + baos.write( NFCUtils.numTo( mMyDevID ) ); } else { MsgToken token = NFCUtils.getMsgsFor( mGameID ); - byte[][] tmp = wrapMsg( token, Short.MAX_VALUE ); + byte[][] tmp = NFCUtils.wrapMsg( token, Short.MAX_VALUE ); Assert.assertTrue( 1 == tmp.length || !BuildConfig.DEBUG ); baos.write( tmp[0] ); } @@ -336,452 +154,4 @@ public class NFCCardService extends HostApduService { Log.d( TAG, "onDeactivated(reason=%s)", str ); } - - private static Map sSentTokens = new HashMap<>(); - private static void removeSentMsgs( Context context, int ack ) - { - MsgToken msgs = null; - if ( 0 != ack ) { - Log.d( TAG, "removeSentMsgs(msgID=%d)", ack ); - synchronized ( sSentTokens ) { - msgs = sSentTokens.remove( ack ); - Log.d( TAG, "removeSentMsgs(): removed %s, now have %s", msgs, keysFor() ); - } - updateStatus( context, false ); - } - if ( null != msgs ) { - msgs.removeSentMsgs(); - } - } - - private static void remember( int msgID, MsgToken msgs ) - { - if ( 0 != msgID ) { - Log.d( TAG, "remember(msgID=%d)", msgID ); - synchronized ( sSentTokens ) { - sSentTokens.put( msgID, msgs ); - Log.d( TAG, "remember(): now have %s", keysFor() ); - } - } - } - - private static String keysFor() - { - String result = ""; - if ( BuildConfig.DEBUG ) { - result = TextUtils.join( ",", sSentTokens.keySet() ); - } - return result; - } - - private static byte[][] sParts = null; - private static int sMsgID = 0; - private synchronized static byte[] reassemble( Context context, byte[] part, - HEX_STR cmd ) - { - return reassemble( context, part, cmd.length() ); - } - - private synchronized static byte[] reassemble( Context context, byte[] part, - int offset ) - { - part = Arrays.copyOfRange( part, offset, part.length ); - return reassemble( context, part ); - } - - private synchronized static byte[] reassemble( Context context, byte[] part ) - { - byte[] result = null; - try { - ByteArrayInputStream bais = new ByteArrayInputStream( part ); - - final int cur = bais.read(); - final int count = bais.read(); - if ( 0 == cur ) { - sMsgID = numFrom( bais ); - int ack = numFrom( bais ); - removeSentMsgs( context, ack ); - } - - boolean inSequence = true; - if ( sParts == null ) { - if ( 0 == cur ) { - sParts = new byte[count][]; - } else { - Log.e( TAG, "reassemble(): out-of-order message 1" ); - inSequence = false; - } - } else if ( cur >= count || count != sParts.length || null != sParts[cur] ) { - // result = HEX_STR.STATUS_FAILED; - inSequence = false; - Log.e( TAG, "reassemble(): out-of-order message 2" ); - } - - if ( !inSequence ) { - sParts = null; // so we can try again later - } else { - // write rest into array - byte[] rest = new byte[bais.available()]; - bais.read( rest, 0, rest.length ); - sParts[cur] = rest; - // Log.d( TAG, "addOrProcess(): added elem %d: %s", cur, DbgUtils.hexDump( rest ) ); - - // Done? Process!! - if ( cur + 1 == count ) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - for ( int ii = 0; ii < sParts.length; ++ii ) { - baos.write( sParts[ii] ); - } - sParts = null; - - result = baos.toByteArray(); - setLatestAck( sMsgID ); - if ( 0 != sMsgID ) { - Log.d( TAG, "reassemble(): done reassembling msgID=%d: %s", - sMsgID, DbgUtils.hexDump(result) ); - } - } - } - } catch ( IOException ioe ) { - Assert.assertFalse( BuildConfig.DEBUG ); - } - return result; - } - - private static AtomicInteger sLatestAck = new AtomicInteger(0); - private static int getLatestAck() - { - int result = sLatestAck.getAndSet(0); - if ( 0 != result ) { - Log.d( TAG, "getLatestAck() => %d", result ); - } - return result; - } - - private static void setLatestAck( int ack ) - { - if ( 0 != ack ) { - Log.e( TAG, "setLatestAck(%d)", ack ); - } - int oldVal = sLatestAck.getAndSet( ack ); - if ( 0 != oldVal ) { - Log.e( TAG, "setLatestAck(%d): dropping ack msgID %d", ack, oldVal ); - } - } - - private static final int HEADER_SIZE = 10; - private static byte[][] wrapMsg( MsgToken token, int maxLen ) - { - byte[] msg = token.getMsgs(); - final int length = null == msg ? 0 : msg.length; - final int msgID = (0 == length) ? 0 : getNextMsgID(); - if ( 0 < msgID ) { - Log.d( TAG, "wrapMsg(%s); msgID=%d", DbgUtils.hexDump( msg ), msgID ); - } - final int count = 1 + (length / (maxLen - HEADER_SIZE)); - byte[][] result = new byte[count][]; - try { - int offset = 0; - for ( int ii = 0; ii < count; ++ii ) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write( HEX_STR.CMD_MSG_PART.asBA() ); - baos.write( (byte)ii ); - baos.write( (byte)count ); - if ( 0 == ii ) { - baos.write( numTo( msgID ) ); - int latestAck = getLatestAck(); - baos.write( numTo( latestAck ) ); - } - Assert.assertTrue( HEADER_SIZE >= baos.toByteArray().length - || !BuildConfig.DEBUG ); - - int thisLen = Math.min( maxLen - HEADER_SIZE, length - offset ); - if ( 0 < thisLen ) { - // Log.d( TAG, "writing %d bytes starting from offset %d", - // thisLen, offset ); - baos.write( msg, offset, thisLen ); - offset += thisLen; - } - byte[] tmp = baos.toByteArray(); - // Log.d( TAG, "wrapMsg(): adding res[%d]: %s", ii, DbgUtils.hexDump(tmp) ); - result[ii] = tmp; - } - remember( msgID, token ); - } catch ( IOException ioe ) { - Assert.assertFalse( BuildConfig.DEBUG ); - } - return result; - } - - public static class Wrapper implements NfcAdapter.ReaderCallback, - NFCUtils.HaveDataListener { - private Activity mActivity; - private boolean mHaveData; - private Procs mProcs; - private NfcAdapter mAdapter; - private int mMinMS = 300; - private int mMaxMS = 500; - private boolean mConnected = false; - private int mMyDevID; - - public interface Procs { - void onReadingChange( boolean nowReading ); - } - - public static Wrapper init( Activity activity, Procs procs, int devID ) - { - Wrapper instance = null; - NfcAdapter adapter = NfcAdapter.getDefaultAdapter( activity ); - if ( null != adapter ) { - instance = new Wrapper( activity, adapter, procs, devID ); - } - Log.d( TAG, "Wrapper.init(devID=%d) => %s", devID, instance ); - return instance; - } - - static void setResumed( Wrapper instance, boolean resumed ) - { - if ( null != instance ) { - instance.setResumed( resumed ); - } - } - - static void setGameID( Wrapper instance, int gameID ) - { - if ( null != instance ) { - instance.setGameID( gameID ); - } - } - - private Wrapper( Activity activity, NfcAdapter adapter, Procs procs, - int devID ) - { - mActivity = activity; - mAdapter = adapter; - mProcs = procs; - mMyDevID = devID; - } - - private void setResumed( boolean resumed ) - { - if ( resumed ) { - startReadModeThread(); - } else { - stopReadModeThread(); - } - } - - @Override - public void onHaveDataChanged( boolean haveData ) - { - if ( mHaveData != haveData ) { - mHaveData = haveData; - Log.d( TAG, "onHaveDataChanged(): mHaveData now %b", mHaveData ); - interruptThread(); - } - } - - private boolean haveData() - { - boolean result = mHaveData; - // Log.d( TAG, "haveData() => %b", result ); - return result; - } - - private int mGameID; - private void setGameID( int gameID ) - { - Log.d( TAG, "setGameID(%d)", gameID ); - mGameID = gameID; - NFCUtils.setHaveDataListener( gameID, this ); - interruptThread(); - } - - private void interruptThread() - { - synchronized ( mThreadRef ) { - if ( null != mThreadRef[0] ) { - mThreadRef[0].interrupt(); - } - } - } - - @Override - public void onTagDiscovered( Tag tag ) - { - mConnected = true; - IsoDep isoDep = IsoDep.get( tag ); - try { - isoDep.connect(); - int maxLen = isoDep.getMaxTransceiveLength(); - Log.d( TAG, "onTagDiscovered() connected; max len: %d", maxLen ); - byte[] aidBytes = Utils.hexStr2ba( BuildConfig.NFC_AID ); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - baos.write( Utils.hexStr2ba( "00A40400" ) ); - baos.write( (byte)aidBytes.length ); - baos.write( aidBytes ); - baos.write( VERSION_1 ); // min - baos.write( VERSION_1 ); // max - baos.write( numTo( mMyDevID ) ); - baos.write( numTo( mGameID ) ); - byte[] msg = baos.toByteArray(); - Assert.assertTrue( msg.length < maxLen || !BuildConfig.DEBUG ); - byte[] response = isoDep.transceive( msg ); - - // The first reply from transceive() is special. If it starts - // with STATUS_SUCCESS then it also includes the version we'll - // be using to communicate, either what we sent over or - // something lower (for older code on the other side), and the - // remote's deviceID - if ( HEX_STR.STATUS_SUCCESS.matchesFrom( response ) ) { - int offset = HEX_STR.STATUS_SUCCESS.length(); - byte version = response[offset++]; - if ( version == VERSION_1 ) { - int[] out = {0}; - offset += numFrom( response, offset, out ); - Log.d( TAG, "onTagDiscovered(): read remote devID: %d", - out[0] ); - runMessageLoop( isoDep, maxLen ); - } else { - Log.e( TAG, "onTagDiscovered(): remote sent version %d, " - + "not %d; exiting", version, VERSION_1 ); - } - } - isoDep.close(); - } catch ( IOException ioe ) { - Log.e( TAG, "got ioe: " + ioe.getMessage() ); - } - - mConnected = false; - interruptThread(); // make sure we leave read mode! - Log.d( TAG, "onTagDiscovered() DONE" ); - } - - private void runMessageLoop( IsoDep isoDep, int maxLen ) throws IOException - { - outer: - for ( ; ; ) { - MsgToken token = NFCUtils.getMsgsFor( mGameID ); - // PENDING: no need for this Math.min thing once well tested - byte[][] toFit = wrapMsg( token, Math.min( 50, maxLen ) ); - for ( int ii = 0; ii < toFit.length; ++ii ) { - byte[] one = toFit[ii]; - Assert.assertTrue( one.length < maxLen || !BuildConfig.DEBUG ); - byte[] response = isoDep.transceive( one ); - if ( ! receiveAny( response ) ) { - break outer; - } - } - } - } - - private boolean receiveAny( byte[] response ) - { - boolean statusOK = HEX_STR.STATUS_SUCCESS.matchesFrom( response ); - if ( statusOK ) { - int offset = HEX_STR.STATUS_SUCCESS.length(); - if ( HEX_STR.CMD_MSG_PART.matchesFrom( response, offset ) ) { - byte[] all = reassemble( mActivity, response, - offset + HEX_STR.CMD_MSG_PART.length() ); - Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); - if ( null != all ) { - addToMsgThread( mActivity, all ); - - } - } - } - if ( !statusOK ) { - Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); - } - return statusOK; - } - - private class ReadModeThread extends Thread { - private boolean mShouldStop = false; - private boolean mInReadMode = false; - private final int mFlags = NfcAdapter.FLAG_READER_NFC_A - | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK; - - @Override - public void run() - { - Log.d( TAG, "ReadModeThread.run() starting" ); - Random random = new Random(); - - while ( !mShouldStop ) { - boolean wantReadMode = mConnected || !mInReadMode && haveData(); - if ( wantReadMode && !mInReadMode ) { - mAdapter.enableReaderMode( mActivity, Wrapper.this, mFlags, null ); - } else if ( mInReadMode && !wantReadMode ) { - mAdapter.disableReaderMode( mActivity ); - } - mInReadMode = wantReadMode; - Log.d( TAG, "run(): inReadMode now: %b", mInReadMode ); - - // Now sleep. If we aren't going to want to toggle read - // mode soon, sleep until interrupted by a state change, - // e.g. getting data or losing connection. - long intervalMS = Long.MAX_VALUE; - if ( (mInReadMode && !mConnected) || haveData() ) { - intervalMS = mMinMS + (Math.abs(random.nextInt()) - % (mMaxMS - mMinMS)); - } - try { - Thread.sleep( intervalMS ); - } catch ( InterruptedException ie ) { - Log.d( TAG, "run interrupted" ); - } - } - - // Kill read mode on the way out - if ( mInReadMode ) { - mAdapter.disableReaderMode( mActivity ); - mInReadMode = false; - } - - // Clear the reference only if it's me - synchronized ( mThreadRef ) { - if ( mThreadRef[0] == this ) { - mThreadRef[0] = null; - } - } - Log.d( TAG, "ReadModeThread.run() exiting" ); - } - - public void doStop() - { - mShouldStop = true; - interrupt(); - } - } - - private ReadModeThread[] mThreadRef = {null}; - private void startReadModeThread() - { - synchronized ( mThreadRef ) { - if ( null == mThreadRef[0] ) { - mThreadRef[0] = new ReadModeThread(); - mThreadRef[0].start(); - } - } - } - - private void stopReadModeThread() - { - ReadModeThread thread; - synchronized ( mThreadRef ) { - thread = mThreadRef[0]; - mThreadRef[0] = null; - } - - if ( null != thread ) { - thread.doStop(); - try { - thread.join(); - } catch ( InterruptedException ex ) { - Log.d( TAG, "stopReadModeThread(): %s", ex ); - } - } - } - } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java index 76a7bbd98..f2cebca5a 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/NFCUtils.java @@ -28,8 +28,11 @@ import android.content.Intent; import android.nfc.NfcAdapter; import android.nfc.NfcEvent; import android.nfc.NfcManager; +import android.nfc.Tag; +import android.nfc.tech.IsoDep; import android.os.Build; import android.os.Parcelable; +import android.text.TextUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -37,29 +40,37 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; +import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Random; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; import org.eehouse.android.xw4.MultiService.MultiEvent; +import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec; import org.eehouse.android.xw4.loc.LocUtils; public class NFCUtils { private static final String TAG = NFCUtils.class.getSimpleName(); + private static final boolean USE_BIGINTEGER = true; private static final String NFC_TO_SELF_ACTION = "org.eehouse.nfc_to_self"; private static final String NFC_TO_SELF_DATA = "nfc_data"; + static final byte VERSION_1 = (byte)0x01; + private static final byte MESSAGE = 0x01; private static final byte INVITE = 0x02; private static final byte REPLY = 0x03; private static final byte REPLY_NOGAME = 0x00; - private static boolean s_inSDK = 14 <= Build.VERSION.SDK_INT; + private static boolean s_inSDK = 19 <= Build.VERSION.SDK_INT; private static boolean[] s_nfcAvail; // Return array of two booleans, the first indicating whether the @@ -430,6 +441,625 @@ public class NFCUtils { } } + static enum HEX_STR { + DEFAULT_CLA( "00" ) + , SELECT_INS( "A4" ) + , STATUS_FAILED( "6F00" ) + , CLA_NOT_SUPPORTED( "6E00" ) + , INS_NOT_SUPPORTED( "6D00" ) + , STATUS_SUCCESS( "9000" ) + , CMD_MSG_PART( "70FC" ) + ; + + private byte[] mBytes; + private HEX_STR( String hex ) { mBytes = Utils.hexStr2ba(hex); } + byte[] asBA() { return mBytes; } + boolean matchesFrom( byte[] src ) + { + return matchesFrom( src, 0 ); + } + boolean matchesFrom( byte[] src, int offset ) + { + boolean result = offset + mBytes.length <= src.length; + for ( int ii = 0; result && ii < mBytes.length; ++ii ) { + result = src[offset + ii] == mBytes[ii]; + } + // Log.d( TAG, "%s.matchesFrom(%s) => %b", this, src, result ); + return result; + } + int length() { return asBA().length; } + } + + private static int sNextMsgID = 0; + private static synchronized int getNextMsgID() + { + return ++sNextMsgID; + } + + static byte[] numTo( int num ) + { + byte[] result; + if ( USE_BIGINTEGER ) { + BigInteger bi = BigInteger.valueOf( num ); + byte[] bibytes = bi.toByteArray(); + result = new byte[1 + bibytes.length]; + result[0] = (byte)bibytes.length; + System.arraycopy( bibytes, 0, result, 1, bibytes.length ); + } else { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream( baos ); + try { + dos.writeInt( num ); + dos.flush(); + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + result = baos.toByteArray(); + } + // Log.d( TAG, "numTo(%d) => %s", num, DbgUtils.hexDump(result) ); + return result; + } + + static int numFrom( ByteArrayInputStream bais ) throws IOException + { + int biLen = bais.read(); + // Log.d( TAG, "numFrom(): read biLen: %d", biLen ); + byte[] bytes = new byte[biLen]; + bais.read( bytes ); + BigInteger bi = new BigInteger( bytes ); + int result = bi.intValue(); + + // Log.d( TAG, "numFrom() => %d", result ); + return result; + } + + static int numFrom( byte[] bytes, int start, int out[] ) + { + int result; + if ( USE_BIGINTEGER ) { + byte biLen = bytes[start]; + byte[] rest = Arrays.copyOfRange( bytes, start + 1, start + 1 + biLen ); + BigInteger bi = new BigInteger(rest); + out[0] = bi.intValue(); + result = biLen + 1; + } else { + ByteArrayInputStream bais = new ByteArrayInputStream( bytes, start, + bytes.length - start ); + DataInputStream dis = new DataInputStream( bais ); + try { + out[0] = dis.readInt(); + } catch ( IOException ioe ) { + Log.e( TAG, "from readInt(): %s", ioe.getMessage() ); + } + result = bais.available() - start; + } + return result; + } + + // private static void testNumThing() + // { + // Log.d( TAG, "testNumThing() starting" ); + + // int[] out = {0}; + // for ( int ii = 1; ii > 0 && ii < Integer.MAX_VALUE; ii *= 2 ) { + // byte[] tmp = numTo( ii ); + // numFrom( tmp, 0, out ); + // if ( ii != out[0] ) { + // Log.d( TAG, "testNumThing(): %d failed; got %d", ii, out[0] ); + // break; + // } else { + // Log.d( TAG, "testNumThing(): %d ok", ii ); + // } + // } + // Log.d( TAG, "testNumThing() DONE" ); + // } + + private static AtomicInteger sLatestAck = new AtomicInteger(0); + static int getLatestAck() + { + int result = sLatestAck.getAndSet(0); + if ( 0 != result ) { + Log.d( TAG, "getLatestAck() => %d", result ); + } + return result; + } + + static void setLatestAck( int ack ) + { + if ( 0 != ack ) { + Log.e( TAG, "setLatestAck(%d)", ack ); + } + int oldVal = sLatestAck.getAndSet( ack ); + if ( 0 != oldVal ) { + Log.e( TAG, "setLatestAck(%d): dropping ack msgID %d", ack, oldVal ); + } + } + + private static void updateStatus( Context context, boolean in ) + { + if ( in ) { + ConnStatusHandler + .updateStatusIn( context, CommsConnType.COMMS_CONN_NFC, true ); + } else { + ConnStatusHandler + .updateStatusOut( context, CommsConnType.COMMS_CONN_NFC, true ); + } + } + + private static Map sSentTokens = new HashMap<>(); + private static void removeSentMsgs( Context context, int ack ) + { + MsgToken msgs = null; + if ( 0 != ack ) { + Log.d( TAG, "removeSentMsgs(msgID=%d)", ack ); + synchronized ( sSentTokens ) { + msgs = sSentTokens.remove( ack ); + Log.d( TAG, "removeSentMsgs(): removed %s, now have %s", msgs, keysFor() ); + } + updateStatus( context, false ); + } + if ( null != msgs ) { + msgs.removeSentMsgs(); + } + } + + private static void remember( int msgID, MsgToken msgs ) + { + if ( 0 != msgID ) { + Log.d( TAG, "remember(msgID=%d)", msgID ); + synchronized ( sSentTokens ) { + sSentTokens.put( msgID, msgs ); + Log.d( TAG, "remember(): now have %s", keysFor() ); + } + } + } + + private static String keysFor() + { + String result = ""; + if ( BuildConfig.DEBUG ) { + result = TextUtils.join( ",", sSentTokens.keySet() ); + } + return result; + } + + private static byte[][] sParts = null; + private static int sMsgID = 0; + synchronized static byte[] reassemble( Context context, byte[] part, + HEX_STR cmd ) + { + return reassemble( context, part, cmd.length() ); + } + + synchronized static byte[] reassemble( Context context, byte[] part, + int offset ) + { + part = Arrays.copyOfRange( part, offset, part.length ); + return reassemble( context, part ); + } + + synchronized static byte[] reassemble( Context context, byte[] part ) + { + byte[] result = null; + try { + ByteArrayInputStream bais = new ByteArrayInputStream( part ); + + final int cur = bais.read(); + final int count = bais.read(); + if ( 0 == cur ) { + sMsgID = NFCUtils.numFrom( bais ); + int ack = NFCUtils.numFrom( bais ); + removeSentMsgs( context, ack ); + } + + boolean inSequence = true; + if ( sParts == null ) { + if ( 0 == cur ) { + sParts = new byte[count][]; + } else { + Log.e( TAG, "reassemble(): out-of-order message 1" ); + inSequence = false; + } + } else if ( cur >= count || count != sParts.length || null != sParts[cur] ) { + // result = HEX_STR.STATUS_FAILED; + inSequence = false; + Log.e( TAG, "reassemble(): out-of-order message 2" ); + } + + if ( !inSequence ) { + sParts = null; // so we can try again later + } else { + // write rest into array + byte[] rest = new byte[bais.available()]; + bais.read( rest, 0, rest.length ); + sParts[cur] = rest; + // Log.d( TAG, "addOrProcess(): added elem %d: %s", cur, DbgUtils.hexDump( rest ) ); + + // Done? Process!! + if ( cur + 1 == count ) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for ( int ii = 0; ii < sParts.length; ++ii ) { + baos.write( sParts[ii] ); + } + sParts = null; + + result = baos.toByteArray(); + setLatestAck( sMsgID ); + if ( 0 != sMsgID ) { + Log.d( TAG, "reassemble(): done reassembling msgID=%d: %s", + sMsgID, DbgUtils.hexDump(result) ); + } + } + } + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + return result; + } + + private static final int HEADER_SIZE = 10; + static byte[][] wrapMsg( MsgToken token, int maxLen ) + { + byte[] msg = token.getMsgs(); + final int length = null == msg ? 0 : msg.length; + final int msgID = (0 == length) ? 0 : getNextMsgID(); + if ( 0 < msgID ) { + Log.d( TAG, "wrapMsg(%s); msgID=%d", DbgUtils.hexDump( msg ), msgID ); + } + final int count = 1 + (length / (maxLen - HEADER_SIZE)); + byte[][] result = new byte[count][]; + try { + int offset = 0; + for ( int ii = 0; ii < count; ++ii ) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write( HEX_STR.CMD_MSG_PART.asBA() ); + baos.write( (byte)ii ); + baos.write( (byte)count ); + if ( 0 == ii ) { + baos.write( numTo( msgID ) ); + int latestAck = getLatestAck(); + baos.write( numTo( latestAck ) ); + } + Assert.assertTrue( HEADER_SIZE >= baos.toByteArray().length + || !BuildConfig.DEBUG ); + + int thisLen = Math.min( maxLen - HEADER_SIZE, length - offset ); + if ( 0 < thisLen ) { + // Log.d( TAG, "writing %d bytes starting from offset %d", + // thisLen, offset ); + baos.write( msg, offset, thisLen ); + offset += thisLen; + } + byte[] tmp = baos.toByteArray(); + // Log.d( TAG, "wrapMsg(): adding res[%d]: %s", ii, DbgUtils.hexDump(tmp) ); + result[ii] = tmp; + } + remember( msgID, token ); + } catch ( IOException ioe ) { + Assert.assertFalse( BuildConfig.DEBUG ); + } + return result; + } + + private static class QueueElem { + Context context; + byte[] msg; + QueueElem( Context pContext, byte[] pMsg ) { + context = pContext; + msg = pMsg; + } + } + + private static LinkedBlockingQueue sQueue = null; + synchronized static void addToMsgThread( Context context, byte[] msg ) + { + if ( 0 < msg.length ) { + QueueElem elem = new QueueElem( context, msg ); + if ( null == sQueue ) { + sQueue = new LinkedBlockingQueue<>(); + new Thread( new Runnable() { + @Override + public void run() { + Log.d( TAG, "addToMsgThread(): run starting" ); + for ( ; ; ) { + try { + QueueElem elem = sQueue.take(); + NFCUtils.receiveMsgs( elem.context, elem.msg ); + updateStatus( elem.context, true ); + } catch ( InterruptedException ie ) { + break; + } + } + Log.d( TAG, "addToMsgThread(): run exiting" ); + } + } ).start(); + } + sQueue.add( elem ); + // } else { + // // This is very common right now + // Log.d( TAG, "addToMsgThread(): dropping 0-length msg" ); + } + } + + public static class Wrapper { + private Reader mReader; + + public interface Procs { + void onReadingChange( boolean nowReading ); + } + + private Wrapper( Activity activity, Procs procs, int devID ) + { + mReader = new Reader( activity, procs, devID ); + } + + public static Wrapper init( Activity activity, Procs procs, int devID ) + { + Wrapper instance = null; + + if ( nfcAvail( activity )[1] ) { + instance = new Wrapper( activity, procs, devID ); + } + Log.d( TAG, "Wrapper.init(devID=%d) => %s", devID, instance ); + return instance; + } + + static void setResumed( Wrapper instance, boolean resumed ) + { + if ( null != instance ) { + instance.mReader.setResumed( resumed ); + } + } + + static void setGameID( Wrapper instance, int gameID ) + { + if ( null != instance ) { + instance.mReader.setGameID( gameID ); + } + } + } + + private static class Reader implements NfcAdapter.ReaderCallback, + HaveDataListener { + private Activity mActivity; + private boolean mHaveData; + private Wrapper.Procs mProcs; + private NfcAdapter mAdapter; + private int mMinMS = 300; + private int mMaxMS = 500; + private boolean mConnected = false; + private int mMyDevID; + + private Reader( Activity activity, Wrapper.Procs procs, int devID ) + { + mActivity = activity; + mProcs = procs; + mMyDevID = devID; + mAdapter = NfcAdapter.getDefaultAdapter( activity ); + } + + private void setResumed( boolean resumed ) + { + if ( resumed ) { + startReadModeThread(); + } else { + stopReadModeThread(); + } + } + + @Override + public void onHaveDataChanged( boolean haveData ) + { + if ( mHaveData != haveData ) { + mHaveData = haveData; + Log.d( TAG, "onHaveDataChanged(): mHaveData now %b", mHaveData ); + interruptThread(); + } + } + + private boolean haveData() + { + boolean result = mHaveData; + // Log.d( TAG, "haveData() => %b", result ); + return result; + } + + private int mGameID; + private void setGameID( int gameID ) + { + Log.d( TAG, "setGameID(%d)", gameID ); + mGameID = gameID; + NFCUtils.setHaveDataListener( gameID, this ); + interruptThread(); + } + + private void interruptThread() + { + synchronized ( mThreadRef ) { + if ( null != mThreadRef[0] ) { + mThreadRef[0].interrupt(); + } + } + } + + @Override + public void onTagDiscovered( Tag tag ) + { + mConnected = true; + IsoDep isoDep = IsoDep.get( tag ); + try { + isoDep.connect(); + int maxLen = isoDep.getMaxTransceiveLength(); + Log.d( TAG, "onTagDiscovered() connected; max len: %d", maxLen ); + byte[] aidBytes = Utils.hexStr2ba( BuildConfig.NFC_AID ); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write( Utils.hexStr2ba( "00A40400" ) ); + baos.write( (byte)aidBytes.length ); + baos.write( aidBytes ); + baos.write( VERSION_1 ); // min + baos.write( VERSION_1 ); // max + baos.write( numTo( mMyDevID ) ); + baos.write( numTo( mGameID ) ); + byte[] msg = baos.toByteArray(); + Assert.assertTrue( msg.length < maxLen || !BuildConfig.DEBUG ); + byte[] response = isoDep.transceive( msg ); + + // The first reply from transceive() is special. If it starts + // with STATUS_SUCCESS then it also includes the version we'll + // be using to communicate, either what we sent over or + // something lower (for older code on the other side), and the + // remote's deviceID + if ( HEX_STR.STATUS_SUCCESS.matchesFrom( response ) ) { + int offset = HEX_STR.STATUS_SUCCESS.length(); + byte version = response[offset++]; + if ( version == VERSION_1 ) { + int[] out = {0}; + offset += numFrom( response, offset, out ); + Log.d( TAG, "onTagDiscovered(): read remote devID: %d", + out[0] ); + runMessageLoop( isoDep, maxLen ); + } else { + Log.e( TAG, "onTagDiscovered(): remote sent version %d, " + + "not %d; exiting", version, VERSION_1 ); + } + } + isoDep.close(); + } catch ( IOException ioe ) { + Log.e( TAG, "got ioe: " + ioe.getMessage() ); + } + + mConnected = false; + interruptThread(); // make sure we leave read mode! + Log.d( TAG, "onTagDiscovered() DONE" ); + } + + private void runMessageLoop( IsoDep isoDep, int maxLen ) throws IOException + { + outer: + for ( ; ; ) { + MsgToken token = NFCUtils.getMsgsFor( mGameID ); + // PENDING: no need for this Math.min thing once well tested + byte[][] toFit = wrapMsg( token, Math.min( 50, maxLen ) ); + for ( int ii = 0; ii < toFit.length; ++ii ) { + byte[] one = toFit[ii]; + Assert.assertTrue( one.length < maxLen || !BuildConfig.DEBUG ); + byte[] response = isoDep.transceive( one ); + if ( ! receiveAny( response ) ) { + break outer; + } + } + } + } + + private boolean receiveAny( byte[] response ) + { + boolean statusOK = HEX_STR.STATUS_SUCCESS.matchesFrom( response ); + if ( statusOK ) { + int offset = HEX_STR.STATUS_SUCCESS.length(); + if ( HEX_STR.CMD_MSG_PART.matchesFrom( response, offset ) ) { + byte[] all = reassemble( mActivity, response, + offset + HEX_STR.CMD_MSG_PART.length() ); + Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); + if ( null != all ) { + addToMsgThread( mActivity, all ); + } + } + } + if ( !statusOK ) { + Log.d( TAG, "receiveAny(%s) => %b", DbgUtils.hexDump( response ), statusOK ); + } + return statusOK; + } + + private class ReadModeThread extends Thread { + private boolean mShouldStop = false; + private boolean mInReadMode = false; + private final int mFlags = NfcAdapter.FLAG_READER_NFC_A + | NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK; + + @Override + public void run() + { + Log.d( TAG, "ReadModeThread.run() starting" ); + Random random = new Random(); + + while ( !mShouldStop ) { + boolean wantReadMode = mConnected || !mInReadMode && haveData(); + if ( wantReadMode && !mInReadMode ) { + mAdapter.enableReaderMode( mActivity, Reader.this, mFlags, null ); + } else if ( mInReadMode && !wantReadMode ) { + mAdapter.disableReaderMode( mActivity ); + } + mInReadMode = wantReadMode; + Log.d( TAG, "run(): inReadMode now: %b", mInReadMode ); + + // Now sleep. If we aren't going to want to toggle read + // mode soon, sleep until interrupted by a state change, + // e.g. getting data or losing connection. + long intervalMS = Long.MAX_VALUE; + if ( (mInReadMode && !mConnected) || haveData() ) { + intervalMS = mMinMS + (Math.abs(random.nextInt()) + % (mMaxMS - mMinMS)); + } + try { + Thread.sleep( intervalMS ); + } catch ( InterruptedException ie ) { + Log.d( TAG, "run interrupted" ); + } + } + + // Kill read mode on the way out + if ( mInReadMode ) { + mAdapter.disableReaderMode( mActivity ); + mInReadMode = false; + } + + // Clear the reference only if it's me + synchronized ( mThreadRef ) { + if ( mThreadRef[0] == this ) { + mThreadRef[0] = null; + } + } + Log.d( TAG, "ReadModeThread.run() exiting" ); + } + + public void doStop() + { + mShouldStop = true; + interrupt(); + } + } + + private ReadModeThread[] mThreadRef = {null}; + private void startReadModeThread() + { + synchronized ( mThreadRef ) { + if ( null == mThreadRef[0] ) { + mThreadRef[0] = new ReadModeThread(); + mThreadRef[0].start(); + } + } + } + + private void stopReadModeThread() + { + ReadModeThread thread; + synchronized ( mThreadRef ) { + thread = mThreadRef[0]; + mThreadRef[0] = null; + } + + if ( null != thread ) { + thread.doStop(); + try { + thread.join(); + } catch ( InterruptedException ex ) { + Log.d( TAG, "stopReadModeThread(): %s", ex ); + } + } + } + } + private static class NFCServiceHelper extends XWServiceHelper { private CommsAddrRec mAddr = new CommsAddrRec( CommsAddrRec.CommsConnType.COMMS_CONN_NFC ); From 15239d4a6c1a68e06ecf53372630d915fb4c710f Mon Sep 17 00:00:00 2001 From: Eric House Date: Sun, 29 Dec 2019 08:33:38 -0800 Subject: [PATCH 19/24] Control showing message count via a prefs checkbox Off by default on non-DEBUG builds, but still available. --- .../java/org/eehouse/android/xw4/ConnStatusHandler.java | 2 +- .../src/main/java/org/eehouse/android/xw4/GameListItem.java | 2 +- .../app/src/main/java/org/eehouse/android/xw4/XWPrefs.java | 6 ++++++ xwords4/android/app/src/main/res/values/common_rsrc.xml | 1 + xwords4/android/app/src/main/res/values/strings.xml | 2 ++ xwords4/android/app/src/main/res/xml/xwprefs.xml | 6 ++++++ 6 files changed, 17 insertions(+), 2 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java index 1b85b209f..633523fc6 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java @@ -464,7 +464,7 @@ public class ConnStatusHandler { - scratchR.height()) ); drawIn( canvas, res, R.drawable.multigame__gen, scratchR ); - if ( BuildConfig.DEBUG && 0 < s_moveCount ) { + if ( 0 < s_moveCount && XWPrefs.moveCountEnabled( context ) ) { String str = String.format( "%d", s_moveCount ); s_fillPaint.setColor( Color.BLACK ); canvas.drawText( str, s_rect.left + (s_rect.width() / 2), diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java index 9390244f8..7fed2a1d3 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/GameListItem.java @@ -335,7 +335,7 @@ public class GameListItem extends LinearLayout findViewById( R.id.has_chat_marker ) .setVisibility( hasChat ? View.VISIBLE : View.GONE ); - if ( BuildConfig.DEBUG ) { + if ( XWPrefs.moveCountEnabled( m_context ) ) { TextView tv = (TextView)findViewById( R.id.n_pending ); int nPending = summary.nPacketsPending; String str = nPending == 0 ? "" : String.format( "%d", nPending ); 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 bcd2ea7cb..cb5f1a372 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 @@ -61,6 +61,12 @@ public class XWPrefs { return getPrefsBoolean( context, R.string.key_enable_dup_invite, false ); } + public static boolean moveCountEnabled( Context context ) + { + return getPrefsBoolean( context, R.string.key_enable_pending_count, + BuildConfig.DEBUG ); + } + public static boolean getIgnoreFCM( Context context ) { String curValue = 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 dce097a73..56e50c8bc 100644 --- a/xwords4/android/app/src/main/res/values/common_rsrc.xml +++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml @@ -126,6 +126,7 @@ key_na_dicts key_enable_debug key_enable_dup_invite + key_enable_pending_count key_enable_sms_toself key_show_fcm2 key_nag_intervals diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 986e328c0..12a670e8d 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -2197,6 +2197,8 @@ Accept duplicate invites Fake locale for translation Accept invitations more than once + Show Pending messages + Show number not yet acknowledged Reminder intervals (minutes1,minutes2,…) Short-circuit SMS to self Skip radio when phone numbers same diff --git a/xwords4/android/app/src/main/res/xml/xwprefs.xml b/xwords4/android/app/src/main/res/xml/xwprefs.xml index 9d72915af..755ac4bbf 100644 --- a/xwords4/android/app/src/main/res/xml/xwprefs.xml +++ b/xwords4/android/app/src/main/res/xml/xwprefs.xml @@ -390,6 +390,12 @@ android:defaultValue="false" /> + + Date: Mon, 30 Dec 2019 10:53:27 -0800 Subject: [PATCH 20/24] remove dead code (old GCM stuff) --- .../java/org/eehouse/android/xw4/DevID.java | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java index 6d291112b..32cfdd7a5 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DevID.java @@ -119,34 +119,6 @@ public class DevID { DBUtils.setBoolFor( context, DEVID_ACK_KEY, false ); } - public static String getGCMDevID( Context context ) - { - return ""; - // int curVers = Utils.getAppVersion( context ); - // int storedVers = DBUtils.getIntFor( context, GCM_REGVERS_KEY, 0 ); - // // TRANSITIONAL - // if ( 0 == storedVers ) { - // storedVers = XWPrefs.getPrefsInt( context, - // R.string.key_gcmvers_regid, 0 ); - // if ( 0 != storedVers ) { - // DBUtils.setIntFor( context, GCM_REGVERS_KEY, storedVers ); - // } - // } - - // String result; - // if ( 0 != storedVers && storedVers < curVers ) { - // result = ""; // Don't trust what registrar has - // } else { - // result = GCMStub.getRegistrationId( context ); - // } - // return result; - } - - public static void clearGCMDevID( Context context ) - { - DBUtils.setBoolFor( context, DEVID_ACK_KEY, false ); - } - // Just a random number I hang onto as long as possible private static int[] sNFCDevID = {0}; public static int getNFCDevID( Context context ) From 278275a9cb583425b138c79e006796a0b6ca8bcd Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 30 Dec 2019 10:37:02 -0800 Subject: [PATCH 21/24] get rid of pixelation using svg instead of png There was only one size of png for the net status arrows. Now there are svgs that generate the whole range. Also set color programatically to avoid having to maintain four. --- .../android/xw4/ConnStatusHandler.java | 26 ++++--- .../app/src/main/res/drawable/in_arrow.png | Bin 135 -> 0 bytes .../src/main/res/drawable/in_arrow_active.png | Bin 146 -> 0 bytes .../app/src/main/res/drawable/out_arrow.png | Bin 135 -> 0 bytes .../main/res/drawable/out_arrow_active.png | Bin 147 -> 0 bytes xwords4/android/img_src/in_arrow.svg | 69 ++++++++++++++++++ xwords4/android/img_src/out_arrow.svg | 69 ++++++++++++++++++ 7 files changed, 154 insertions(+), 10 deletions(-) delete mode 100644 xwords4/android/app/src/main/res/drawable/in_arrow.png delete mode 100644 xwords4/android/app/src/main/res/drawable/in_arrow_active.png delete mode 100644 xwords4/android/app/src/main/res/drawable/out_arrow.png delete mode 100644 xwords4/android/app/src/main/res/drawable/out_arrow_active.png create mode 100644 xwords4/android/img_src/in_arrow.svg create mode 100644 xwords4/android/img_src/out_arrow.svg diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java index 633523fc6..cb39871c3 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/ConnStatusHandler.java @@ -30,6 +30,7 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.provider.Settings; import android.text.format.DateUtils; +import android.graphics.PorterDuff.Mode; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType; import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnTypeSet; @@ -47,6 +48,7 @@ public class ConnStatusHandler { private static final String TAG = ConnStatusHandler.class.getSimpleName(); private static final String RECS_KEY = TAG + "/recs"; private static final String STALL_STATS_KEY = TAG + "/stall_stats"; + private static final int ORANGE = 0XFFFFA500; public interface ConnStatusCBacks { public void invalidateParent(); @@ -487,16 +489,10 @@ public class ConnStatusHandler { private static void drawArrow( Canvas canvas, Resources res, Rect rect, boolean isIn ) { - int arrowID; boolean showSuccesses = s_showSuccesses[isIn? SUCCESS_IN : SUCCESS_OUT]; - if ( isIn ) { - arrowID = showSuccesses ? - R.drawable.in_arrow_active : R.drawable.in_arrow; - } else { - arrowID = showSuccesses ? - R.drawable.out_arrow_active : R.drawable.out_arrow; - } - drawIn( canvas, res, arrowID, rect ); + int color = showSuccesses ? ORANGE : Color.WHITE; + int arrowID = isIn ? R.drawable.in_arrow__gen : R.drawable.out_arrow__gen; + drawIn( canvas, res, arrowID, rect, color ); } // This gets rid of lint warning, but I don't like it as it @@ -576,9 +572,19 @@ public class ConnStatusHandler { } private static void drawIn( Canvas canvas, Resources res, int id, Rect rect ) + { + drawIn( canvas, res, id, rect, Color.WHITE ); + } + + private static void drawIn( Canvas canvas, Resources res, int id, Rect rect, int color ) { Drawable icon = res.getDrawable( id ); - Assert.assertTrue( icon.getBounds().width() == icon.getBounds().height() ); + if ( Color.WHITE != color ) { + icon = icon.mutate(); + icon.setColorFilter( color, Mode.MULTIPLY ); + } + Assert.assertTrue( icon.getBounds().width() == icon.getBounds().height() + || !BuildConfig.DEBUG ); icon.setBounds( rect ); icon.draw( canvas ); } diff --git a/xwords4/android/app/src/main/res/drawable/in_arrow.png b/xwords4/android/app/src/main/res/drawable/in_arrow.png deleted file mode 100644 index aa6d87f03cd822718d85ebf739dc6c0626a488d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~p!3HE570x;VDYhhUcNd2LAh=-f^2tCE8&4O< z5Q)plDL>9T2qq*ZBqaPe|6#sPvV^3BM1?Q|&0+9#^>bP0l+XkKfeI+I diff --git a/xwords4/android/app/src/main/res/drawable/in_arrow_active.png b/xwords4/android/app/src/main/res/drawable/in_arrow_active.png deleted file mode 100644 index fac65b7a6d68e00a30a8c8b2e33190db55d61452..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~p!3HE570x<=sNx`ZCx&(BWL^R}Y)RhkE)4%c zaKYZ?lYt^uo-U3d5|@)xew=p@Oh`;fNceHzd!Av>QJx+j1Gb%d2`<~Dp9ux=q&Sre ot&B@LvP|SAgVVaiB_?7F>*@vSUW5wG1)9R(>FVdQ&MBb@0R6r!u>b%7 diff --git a/xwords4/android/app/src/main/res/drawable/out_arrow.png b/xwords4/android/app/src/main/res/drawable/out_arrow.png deleted file mode 100644 index 4ee78c0519f741b99d64fa9f768293227510087c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~p!3HE570x;VDYhhUcNd2LAh=-f^2tCE8&4O< z5Q)plDL>9T2qq{ld0>CIeuroePfyQ_Pd!I=F{BCXQuH}uCAv)TrfY%t0`5sk3c@=Z f+dl}kZe(EKiV5^{)z4*}Q$iB})TAm^ diff --git a/xwords4/android/app/src/main/res/drawable/out_arrow_active.png b/xwords4/android/app/src/main/res/drawable/out_arrow_active.png deleted file mode 100644 index 46e0c14aed98b49f09bec0828a48fc7c316aef45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~p!3HE570x<=sNx`ZCx&(BWL^R}Y)RhkE)4%c zaKYZ?lYt`Eo-U3d5|@)xew=p@Oi*0%pkDtF*Rdovw~|V?q*w-Hj=KVCNpra_cf3*B p!F_?%({MuPodeC)otGjQ7%uG*nv(tYZ41yC22WQ%mvv4FO#nFvF9-kt diff --git a/xwords4/android/img_src/in_arrow.svg b/xwords4/android/img_src/in_arrow.svg new file mode 100644 index 000000000..c1e13c448 --- /dev/null +++ b/xwords4/android/img_src/in_arrow.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/xwords4/android/img_src/out_arrow.svg b/xwords4/android/img_src/out_arrow.svg new file mode 100644 index 000000000..3cd4537d4 --- /dev/null +++ b/xwords4/android/img_src/out_arrow.svg @@ -0,0 +1,69 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + From c663d33f88719c4694a35382fb6098fd0d6196a7 Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 30 Dec 2019 22:06:24 -0800 Subject: [PATCH 22/24] fix string script to detect mismatch of format specifiers Crash due to typo introduced by Weblate made me realize I wasn't running the script that ensures translated strings have the same set of format specifiers as the English -- which might not be strictly necessary, but it's safer to enforce it for now. Moved a few remaining %s to %1$s style while at it. --- .../app/src/main/res/values/strings.xml | 12 ++-- .../res_src/values-b+zh+HANS+CN/strings.xml | 2 +- .../android/res_src/values-ba_CK/strings.xml | 2 +- xwords4/android/res_src/values-ca/strings.xml | 30 ++++----- .../android/res_src/values-ca_PS/strings.xml | 2 +- xwords4/android/res_src/values-cs/strings.xml | 4 +- xwords4/android/res_src/values-de/strings.xml | 26 ++++---- xwords4/android/res_src/values-es/strings.xml | 6 +- xwords4/android/res_src/values-fr/strings.xml | 22 +++---- xwords4/android/res_src/values-ja/strings.xml | 12 +--- .../android/res_src/values-nb-rNO/strings.xml | 18 ++---- xwords4/android/res_src/values-nl/strings.xml | 28 ++++---- xwords4/android/res_src/values-pl/strings.xml | 15 ++--- xwords4/android/res_src/values-pt/strings.xml | 8 +-- xwords4/android/res_src/values-sk/strings.xml | 1 - xwords4/android/scripts/copy-strings.py | 64 +++++++++++++++---- 16 files changed, 133 insertions(+), 119 deletions(-) diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 12a670e8d..66c9a69ca 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -58,7 +58,7 @@ how far along they are. I may list "tiles left" someday instead... --> - %1$d move played + One move played %1$d moves played - This game is waiting for %1$d remote + This game is waiting for one remote player. Would you like to invite someone to join -- assuming you haven\'t already? This game is waiting for %1$d remote @@ -677,6 +677,7 @@ + One tile left in pool. %1$d tiles left in pool. @@ -1845,9 +1846,9 @@ Tap to download and install - [Winner] %s: %d - [Resigned] %s: %d - [#%d] %s: %d + [Winner] %1$s: %2$d + [Resigned] %1$s: %2$d + [#%1$d] %2$s: %3$d You and the host of this game are using different versions of the wordlist %1$s. @@ -2078,6 +2079,7 @@ %1$s passed (0 points) + %1$s played %2$s for one point %1$s played %2$s for %3$d points diff --git a/xwords4/android/res_src/values-b+zh+HANS+CN/strings.xml b/xwords4/android/res_src/values-b+zh+HANS+CN/strings.xml index f0e3bda09..f8a7fa5da 100644 --- a/xwords4/android/res_src/values-b+zh+HANS+CN/strings.xml +++ b/xwords4/android/res_src/values-b+zh+HANS+CN/strings.xml @@ -24,7 +24,7 @@ %1$s(%2$d) %1$s: 设置 - 【#%d】%s:%d + 【#%1$d】%2$s:%3$d 我的游戏 新游戏 diff --git a/xwords4/android/res_src/values-ba_CK/strings.xml b/xwords4/android/res_src/values-ba_CK/strings.xml index 4c86b2fbe..aa23c28d0 100644 --- a/xwords4/android/res_src/values-ba_CK/strings.xml +++ b/xwords4/android/res_src/values-ba_CK/strings.xml @@ -59,7 +59,7 @@ how far along they are. I may list "tiles left" someday instead... --> - %1$d evom deyalp + 1 evom deyalp %1$d sevom deyalp - %1$d MOVE PLAYED + 1 MOVE PLAYED %1$d MOVES PLAYED - %1$d coup joué + 1 coup joué %1$d coups joués - %1$d jeton restant dans le sac. + 1 jeton restant dans le sac. %1$d jetons restants dans le sac. @@ -774,7 +774,7 @@ tous les chevalets :\n--> Un jeton restant dans les chevalets cachés : \n - %d jetons restants dans le sac et les chevalets cachés : + %1$d jetons restants dans le sac et les chevalets cachés : \n Toucher pour télécharger et installer - [Vainqueur] %s : %d + [Vainqueur] %1$s : %2$d - [Abandon] %s : %d + [Abandon] %1$s : %2$d - [%de] %s : %d + [%1$de] %2$s : %3$d @@ -2653,7 +2653,7 @@ pour voir ce qui est disponible. - %1$s a joué %2$s pour %3$d point + %1$s a joué %2$s pour un point %1$s a joué %2$s pour %3$d points @@ -2763,7 +2763,7 @@ d\'autres joueurs quand elle sera ouverte.) - %1$d joueur + 1 joueur %1$d joueurs @@ -3138,10 +3138,6 @@ Merci de me faire savoir si vous aimez cette fonctionnalité, de signaler les pl Wifi Direct En appairer d\'autres Invitation par WiFi direct - - Sélectionnez le nom de l\'appareil WiFi Direct que vous souhaitez inviter à votre nouvelle partie, puis touchez \"%2$s\". - Sélectionnez les noms des appareils WiFi Direct que vous souhaitez inviter à votre nouvelle partie, puis touchez \"%2$s\". - Seuls les appareils actuellement disponibles sont affichés. Si un appareil à proximité ne s\'affiche pas, assurez-vous que le WiFi est allumé, que CrossWords est installé, et que le WiFi Direct est activé. Il n\'y a actuellement pas d\'appareils accessibles par WiFi Direct qui ont CrossWords installé. Utiliser le WiFi Direct pour jouer contre un appareil faisant du WiFi Direct, sur lequel CrossWords est installé. @@ -3205,7 +3201,7 @@ Vous pouvez la ré-ouvrir pour que la permission soit redemandée. Ou vous pouve Rescanner Êtes-vous sûr de vouloir oublier l\'appareil sélectionné \? - "Êtes-vous sûr de vouloir oublier les %1d appareils sélectionnés \?" + "Êtes-vous sûr de vouloir oublier les %1$d appareils sélectionnés \?" " \n diff --git a/xwords4/android/res_src/values-ja/strings.xml b/xwords4/android/res_src/values-ja/strings.xml index 40e14fde2..37943147c 100644 --- a/xwords4/android/res_src/values-ja/strings.xml +++ b/xwords4/android/res_src/values-ja/strings.xml @@ -331,7 +331,7 @@ ゲームの履歴 最終スコア 辞めてもよろしいですか? - [辞めました] %s: %d + [辞めました] %1$s: %2$d 質問… ヒントがあります 今後表示しない @@ -505,8 +505,8 @@ タップすると %1$s を更新 新しいバージョンの %1$s タップすると、ダウンロードとインストール - [勝ち] %s: %d - [#%d] %s: %d + [勝ち] %1$s: %2$d + [#%1$d] %2$s: %3$d あなたとこのゲームのホストは、異なるバージョンの単語リスト %1$s を使用しています。 単語リストの不一致 あなたは単語リスト %1$s を使用していますが、ゲームのホストは %2$s を使用しています。%3$s も使用しますか? @@ -853,12 +853,6 @@ Wifi ダイレクト ペアの詳細 WiFi ダイレクトへの招待 - - 新しいゲームに招待する WiFi -ダイレクトデバイスの名前を選択し、 -\"%2$s\" をタップしてください。 - - 現在利用可能なデバイスだけが表示されます。 近くのデバイス表示されていない場合、WiFi がオンになっていること、 クロスワードがインストールされていること、WiFi ダイレクトのプレイが diff --git a/xwords4/android/res_src/values-nb-rNO/strings.xml b/xwords4/android/res_src/values-nb-rNO/strings.xml index 732f98dd4..1ed6c9430 100644 --- a/xwords4/android/res_src/values-nb-rNO/strings.xml +++ b/xwords4/android/res_src/values-nb-rNO/strings.xml @@ -12,7 +12,7 @@ Spillere invitert til rom \"%1$s\" Spill over - %1$d trekk gjort + 1 trekk gjort %1$d trekk gjort Slett @@ -388,7 +388,7 @@ Siste advarsel: %1$s Din motstander - %1$s spilte %2$s for %3$d poeng + %1$s spilte %2$s for 1 poeng %1$s mistet en tur @@ -566,10 +566,6 @@ La oss spille CrossWords (rom %1$s) Send invitasjon via %1$s e-post - - Er du sikker på at du ønsker å bytte valgt fil (%2$s)? - Er du sikker på at du ønsker å bytte valgt filer (%2$s)? - Sludring for %1$s Tomme felter kan ikke dele spilte flis. Må spille to eller flere flis i første trekk. @@ -623,8 +619,8 @@ \nDu kan skru på spill via SMS nå, eller senere. Skru på relé-spill Skru av relé-spill - [Seierherre] %s: %d - [Resignert] %d: %d + [Seierherre] %1$s: %2$d + [Resignert] %1$s: %2$d Samsvarte ikke med ordliste Gjeninnlaster spill med %1$s (Ikke i eksternt-/SD-kort -minne) @@ -762,7 +758,7 @@ Du kan skru på relé-spill nå, eller senere. Du kan skru på relé-spill nå, eller senere, eller fjerne det fra dette spillet. Er du sikker på at du ønsker å skru av spill på relé-et - [Nr.%d] %s: %d + [Nr.%1$d] %2$s: %3$d Du og verten for dette spillet bruker forskjellige versjoner av ordlisten %1$s. Selv om de kan være høyere NFC er skrudd av på denne enheten. Du kan bruke systeminnstillingene for å skru det på. @@ -796,10 +792,6 @@ \n \n(Sletting av arkivgruppen er trygt fordi den vil bli gjenopprettet igjen når det trengs.) Plukk flis med bokstavene avdekt - - Velg navnet på Wi-Fi Direct-enheten du ønsker å invitere til spillet ditt, trykk så \"%2$s\". - Velg navnet på Wi-Fi Direct-enhetene du ønsker å invitere til spillet ditt, trykk så \"%2$s\". - Kun spillere som er tilgjengelig vises. Hvis en enhet i nærheten ikke vises, forsikre deg om at Wi-Fi er slått på, og at CrossWords er installert, og at spill via Wi-Fi Direct er påslått. Listen over enheter er tom. Bruk \"Skann spill\"-knappen for å skanne dine gamle spill for motstandere. Bruk \"+\"knappen for å skrive inn enhets-ID-er direkte. diff --git a/xwords4/android/res_src/values-nl/strings.xml b/xwords4/android/res_src/values-nl/strings.xml index 30c3a3820..d0520ccd4 100644 --- a/xwords4/android/res_src/values-nl/strings.xml +++ b/xwords4/android/res_src/values-nl/strings.xml @@ -15,7 +15,7 @@ Spel voorbij is kamer \"%1$s\" Spel voorbij - %1$d zet gespeeld + 1 zet gespeeld %1$d zetten gespeeld Verwijder @@ -41,11 +41,11 @@ Verander de naam van dit spel naar: Verander de naam van dit spel (op dit apparaat) naar: - Weet je zeker dat je %1$d geselecteerd spel wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden. + Weet je zeker dat je 1 geselecteerd spel wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden. Weet je zeker dat je %1$d geselecteerde spellen wilt verwijderen? Deze actie kan niet ongedaan gemaakt worden. - Weet je zeker dat je %1$d geselecteerd spel wilt resetten?\n\n(Resetten verwijderd alle zetten en verbindingsinformatie.) + Weet je zeker dat je 1 geselecteerd spel wilt resetten?\n\n(Resetten verwijderd alle zetten en verbindingsinformatie.) Weet je zeker dat je %1$d geselecteerde spellen wilt resetten?\n\n(Resetten verwijderd alle zetten en verbindingsinformatie.) CrossWords Woordenlijsten @@ -120,14 +120,14 @@ 3W (Nog geen zetten gedaan) - Dit spel wacht op %1$d speler op afstand. Wil je iemand uitnodigen om mee te doen, indien je dit nog niet gedaan hebt? + Dit spel wacht op 1 speler op afstand. Wil je iemand uitnodigen om mee te doen, indien je dit nog niet gedaan hebt? Dit spel wacht op %1$d spelers op afstand. Wil je iemand uitnodigen om mee te doen, indien je dit nog niet gedaan hebt? Of druk om uit te nodigen, indien het andere apparaat dichtbij is en Android Beam (NFC) ondersteund. " (Je verwacht meerdere spelers op afstand. Je hoeft ze niet allemaal tegelijk uit te nodigen, maar dit bericht zal niet verdwijnen totdat iedereen uitgenodigd is en de uitnodiging geaccepteerd heeft.)" pntn - Apparaat %1$d verbonden met relay in kamer \"%2$s\". Wachtend op %3$d speler. + Apparaat 1 verbonden met relay in kamer \"%2$s\". Wachtend op %1$d speler. Apparaat %1$d verbonden met relay in kamer \"%2$s\". Wachtend op %3$d spelers. Alle spelers spelers zijn hier in kamer \"%1$s\". @@ -181,11 +181,11 @@ Bonus voor het gebruiken van alle letters: 50\n Score voor beurt: %1$d\n - %1$d letter in de zak. + 1 letter in de zak. %1$d letters in de zak. - %1$d letter in de zak en op alle rekken:\n + 1 letter in de zak en op alle rekken:\n %1$d letters in de zak en op alle rekken:\n Weet je zeker dat je de laatste zet ongedaan wilt maken? (Je kan deze niet opnieuw uitvoeren.) @@ -439,7 +439,7 @@ %1$s heeft je uitgenodigd om te spelen Importeer contact - Selecteer %1$d telefoonnummer dat je voor je nieuwe spel uit wilt nodigen en druk op \"%2$s\". + Selecteer 1 telefoonnummer dat je voor je nieuwe spel uit wilt nodigen en druk op \"%2$s\". Selecteer de %1$d telefoonnummers die je voor je nieuwe spel uit wilt nodigen en druk op \"%2$s\". (Niet in contacten) @@ -496,12 +496,12 @@ Druk om %1$s te updaten Nieuwe versie van %1$s Druk om te downloaden en te installeren - [Winnaar] %s: %d - [Opgegeven] %s: %d - [#%d] %s: %d + [Winnaar] %1$s: %2$d + [Opgegeven] %1$s: %2$d + [#%1$d] %2$s: %3$d Jij en de host van dit spel gebruiken een verschillende versie van de woordenlijst %1$s. Woordenlijsten komen niet overeen - Jij gebruikt woordenlijst %1$s, maar de host gebruikt %2$s. Wil je ook %2$s gebruiken? + Jij gebruikt woordenlijst %1$s, maar de host gebruikt %2$s. Wil je ook %3$s gebruiken? " (Je zult deze eerst moeten downloaden.)" Spel herladen met %1$s Wachten op speluitnodiging @@ -661,7 +661,7 @@ Jouw tegenstander %1$s heeft de beurt overgeslagen (0 punten) - %1$s speelde %2$s voor %3$d punt + %1$s speelde %2$s voor 1 punt %1$s speelde %2$s voor %3$d punten @@ -682,7 +682,7 @@ Standaard Hoeveelheid op dit apparaat - %1$d speler + 1 speler %1$d spelers Dubbele uitnodiging geweigerd: apparaat \"%1$s\" heeft al een uitnodiging voor dit spel geaccepteerd. diff --git a/xwords4/android/res_src/values-pl/strings.xml b/xwords4/android/res_src/values-pl/strings.xml index b5394f6bd..5522c1ace 100644 --- a/xwords4/android/res_src/values-pl/strings.xml +++ b/xwords4/android/res_src/values-pl/strings.xml @@ -217,7 +217,7 @@ Używasz domyślnej nazwy gracza \"%1$s\". Czy chciałbyś spersonalizować swoje imię przed utworzeniem tej gry\? %1$s (robot) - %1$d ruch zagrany + 1 ruch zagrany %1$d ruchy zagrane %1$d ruchów zagranych @@ -323,7 +323,7 @@ \n przenieś (od %1$s w poprzek) \n - przenieś (z %1$ w dół) + przenieś (z %1$s w dół) \n Półka na początku: %1$s \n @@ -569,9 +569,8 @@ Stuknij, aby zaktualizować %1$s Nowa wersja %1$s Stuknij, aby pobrać i zainstalować - [Zwycięzca] %1$s - [Zrezygnował] %s: %d - [#%d] %s: %d + [Zrezygnował] %1$s: %2$d + [#%1$d] %2$s: %3$d Ty i gospodarz tej gry używacie różnych wersji listy słów %1$s. Niedopasowanie listy słów Używasz listy słów %1$s, ale gospodarz gry używa %2$s. Czy chciałbyś również użyć %3$s\? @@ -709,7 +708,7 @@ Twój przeciwnik %1$s minęło (0 punktów) - %1$s zagrał %2$s za %3$d punktów + %1$s zagrał %2$s za 1 punktów @@ -905,7 +904,7 @@ Wybierz odkryte kafelki Wymieszaj kafelki " %." - Dodaj %1$ do listy nauki + Dodaj %1$s do listy nauki Włącz listę nauki Lista do nauczenia… %1$s dodane do %2$s listy do nauki @@ -1003,4 +1002,4 @@ Możesz spojrzeć na słowa PRZED tym, jak są one popełnione jako ruchy - przez długie podsłuchiwanie, tak samo jak słowa popełnione. \n \nUżyj tej funkcji, aby sprawdzić poprawność słów, którymi zamierzasz zagrać, lub poszukaj nieznanego słowa podanego jako wskazówka. - \ No newline at end of file + diff --git a/xwords4/android/res_src/values-pt/strings.xml b/xwords4/android/res_src/values-pt/strings.xml index 1b3e8786e..9af3a7ea5 100644 --- a/xwords4/android/res_src/values-pt/strings.xml +++ b/xwords4/android/res_src/values-pt/strings.xml @@ -1744,17 +1744,17 @@ Listas de palavras Altere o nome deste jogo (apenas neste dispositivo) para: - Você tem certeza que deseja excluir o %1$d jogo selecionado? Esta ação não poderá ser desfeita. + Você tem certeza que deseja excluir o 1 jogo selecionado? Esta ação não poderá ser desfeita. Você tem certeza que deseja excluir os %1$d jogos selecionados? Esta ação não poderá ser desfeita. - Você tem certeza de que deseja redefinir o %1$d jogo selecionado?\n\n(Redefinir apaga todos os movimentos e as informações de conexão.) + Você tem certeza de que deseja redefinir o 1 jogo selecionado?\n\n(Redefinir apaga todos os movimentos e as informações de conexão.) Você tem certeza de que deseja redefinir os %1$d jogos selecionados?\n\n(Redefinir apaga todos os movimentos e as informações de conexão.) Jogadores convidados Jogadores convidados para a sala \"%1$s\" - %1$d movimento jogado + 1 movimento jogado %1$d movimenotos jogados @@ -1765,7 +1765,7 @@ Para quais jogadores deve a list de palavras %1$s ser o predefinição para os jogos novos\? (O idioma de %2$s será a predefinição para ambos.) Conexão (através de %1$s) - Este jogo está aguardando %1$d jogador remoto. Gostaria de convidar alguém para se juntar -- supondo que você ainda não o fez\? + Este jogo está aguardando 1 jogador remoto. Gostaria de convidar alguém para se juntar -- supondo que você ainda não o fez\? Este jogo está aguardando %1$d jogadores remotos. Gostaria de convidar alguém para se juntar -- supondo que você ainda não o fez\? Conexões… diff --git a/xwords4/android/res_src/values-sk/strings.xml b/xwords4/android/res_src/values-sk/strings.xml index fbb0d6d85..ed75d7ca9 100644 --- a/xwords4/android/res_src/values-sk/strings.xml +++ b/xwords4/android/res_src/values-sk/strings.xml @@ -950,7 +950,6 @@ XLATE ME: This game has been deleted on another device. You will not be able to play any further. - Slovo[á] %1$s nebolo nájdené v slovníku. Chcete napriek tomu potvrdiť tento ťah? Ťah stratený. Neplatné slovo[á] diff --git a/xwords4/android/scripts/copy-strings.py b/xwords4/android/scripts/copy-strings.py index 8dcd73644..8b6ed1669 100755 --- a/xwords4/android/scripts/copy-strings.py +++ b/xwords4/android/scripts/copy-strings.py @@ -12,6 +12,7 @@ s_prefix = 'XLATE ME: ' # languages in which it's ok to make a standalone quantity="one" into # quantity="other" g_oneToOthers = ['values-ja'] +g_formatsPat = re.compile( '(%\d\$[sd])', re.DOTALL | re.MULTILINE ) sComment = """ DO NOT EDIT THIS FILE!!!! @@ -19,6 +20,10 @@ sComment = """ Any changes you make to it will be lost. """ +def exitWithError(msg): + print 'ERROR:', msg + sys.exit(1) + def usage(): print "usage:", sys.argv[0], '[-k ]' sys.exit(1) @@ -85,9 +90,8 @@ def pluralsIsSame(engNames, plurals): for item in plurals.getchildren(): text = item.text if not text or 0 == len(text): - print "bogus empty plurals item in", plurals.get('name') + exitWithError( "bogus empty plurals item in " + plurals.get('name')) engItem = engItem - sys.exit(1) quantity = item.get('quantity') if quantity in strings: @@ -119,8 +123,7 @@ def checkPlurals( engNames, elem, src, verbose ): for item in elem.getchildren(): if 0 == len(item.text): ok = False - print 'bad empty item', name - sys.exit(1) + exitWithError( 'bad empty item ' + name ) return ok def loadPlural(plural): @@ -138,11 +141,15 @@ def writeDoc(doc, src, dest): out = open( dest, "w" ) out.write( etree.tostring( doc, pretty_print=True, encoding="utf-8", xml_declaration=True ) ) +def exitWithFormatError(engSet, otherSet, name, path): + exitWithError( 'formats set mismatch: ' + str(engSet) \ + + ' vs ' + str(otherSet) + '; ' + name \ + + ' in file ' + path ) + def checkOrConvertString(engNames, elem, verbose): name = elem.get('name') if not elem.text: - print "ERROR: elem", name, "is empty" - sys.exit(1) + exitWithError( 'elem' + name + " is empty" ) elif not name in engNames or elem.text.startswith(s_prefix): ok = False elif not 'string' == engNames[name]['type']: @@ -167,7 +174,7 @@ def checkOrConvertString(engNames, elem, verbose): ok = True return ok -def checkAndCopy( parser, engNames, src, dest, verbose ): +def checkAndCopy( parser, engNames, engFormats, src, dest, verbose ): doc = etree.parse(src, parser) # strings @@ -179,8 +186,43 @@ def checkAndCopy( parser, engNames, src, dest, verbose ): if not checkPlurals(engNames, elem, src, verbose): elem.getparent().remove(elem) + formats = getFormats( doc, src ) + for name in formats: + if name in formats and not engFormats[name] == formats[name]: + exitWithFormatError( engFormats[name], formats[name], name, dest ) + writeDoc(doc, src, dest) +def setForElem( elem, name ): + result = set() + splits = re.split( g_formatsPat, elem.text ) + nParts = len(splits) + if 1 < nParts: + for ii in range(nParts): + part = splits[ii] + if re.match( g_formatsPat, part ): + result.add( part ) + # print 'setForElem(', name, ') =>', result + return result + +def getFormats( doc, path ): + result = {} + for elem in doc.findall('string'): + name = elem.get('name') + result[name] = setForElem( elem, name ) + for elem in doc.findall('plurals'): + name = elem.get('name') + for item in elem.findall('item'): + quantity = item.get('quantity') + if not item.text or 0 == len(item.text): + exitWithError( 'plurals ' + name + ' has empty quantity ' + quantity \ + + ' in file ' + lang ) + else: + add = name + '/' + quantity + result[add] = setForElem( item, add ) + # print 'getFormats(', path, ') => ', result + return result + def main(): # add these via params later excepts = ['values-ca_PS', 'values-ba_CK'] @@ -198,12 +240,12 @@ def main(): # summarize the english file wd = os.path.dirname(sys.argv[0]) path = wd + '/../app/src/main/res/values/strings.xml' - engNames = {} - engFormats = {} parser = etree.XMLParser(remove_blank_text=True, encoding="utf-8") engDoc = etree.parse(path, parser) - pat = re.compile( '(%\d\$[sd])', re.DOTALL | re.MULTILINE ) + engFormats = getFormats( engDoc, path ) + + engNames = {} for typ in ['string', 'plurals']: for elem in engDoc.findall(typ): name = elem.get('name') @@ -227,7 +269,7 @@ def main(): verbose = 0 == len(verboses) or 0 < len([verb for verb in verboses if verb in path]) print "*** looking at %s ***" % (path) dest = path.replace( 'res_src', 'app/src/main/res', 1 ) - checkAndCopy( parser, engNames, path, dest, verbose ) + checkAndCopy( parser, engNames, engFormats, path, dest, verbose ) ############################################################################## if __name__ == '__main__': From 864648c5b93521bdbad8381fa5f5df8cbfe01076 Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 30 Dec 2019 22:12:19 -0800 Subject: [PATCH 23/24] up changes for new release --- xwords4/android/app/build.gradle | 4 ++-- xwords4/android/app/src/main/assets/changes.html | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/xwords4/android/app/build.gradle b/xwords4/android/app/build.gradle index 32c3a4842..00aecdfef 100644 --- a/xwords4/android/app/build.gradle +++ b/xwords4/android/app/build.gradle @@ -1,6 +1,6 @@ def INITIAL_CLIENT_VERS = 9 -def VERSION_CODE_BASE = 147 -def VERSION_NAME = '4.4.151' +def VERSION_CODE_BASE = 148 +def VERSION_NAME = '4.4.152' def FABRIC_API_KEY = System.getenv("FABRIC_API_KEY") def BUILD_INFO_NAME = "build-info.txt" diff --git a/xwords4/android/app/src/main/assets/changes.html b/xwords4/android/app/src/main/assets/changes.html index 2d286cf3b..28a3ab1f7 100644 --- a/xwords4/android/app/src/main/assets/changes.html +++ b/xwords4/android/app/src/main/assets/changes.html @@ -13,11 +13,9 @@ -

      CrossWords 4.4.151 release

      +

      CrossWords 4.4.152 release

      -

      This release fixes a bad crash, adds a lot of translations in - several languages, and lets you invite and exchange moves via - NFC on any Android version.

      +

      This release fixes a nasty crash in some non-English versions.

      -

      New with this release

      +

      New with this (and previous) release

        +
      • Fix crash with some localized versions (Weblate problem...)
      • +
      • Tweak network traffic arrows on game board
      • Fix crash dragging tiles
      • Fix invite-via-NFC for Android 10 (and improve for earlier Android versions)
      • From 1eb3c7e2e05d6d7fc114bb451d32cdcefd75c818 Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 30 Dec 2019 17:52:28 -0800 Subject: [PATCH 24/24] Write relayID conditionally so we can drop it later --- xwords4/common/nli.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/xwords4/common/nli.c b/xwords4/common/nli.c index 1da8ebf6f..a49802443 100644 --- a/xwords4/common/nli.c +++ b/xwords4/common/nli.c @@ -89,7 +89,9 @@ nli_saveToStream( const NetLaunchInfo* nli, XWStreamCtxt* stream ) if ( types_hasType( nli->_conTypes, COMMS_CONN_RELAY ) ) { stringToStream( stream, nli->room ); stringToStream( stream, nli->inviteID ); - stream_putU32( stream, nli->devID ); + if ( 0 == NLI_VERSION ) { + stream_putU32( stream, nli->devID ); + } } if ( types_hasType( nli->_conTypes, COMMS_CONN_BT ) ) { stringToStream( stream, nli->btName ); @@ -128,7 +130,9 @@ nli_makeFromStream( NetLaunchInfo* nli, XWStreamCtxt* stream ) if ( types_hasType( nli->_conTypes, COMMS_CONN_RELAY ) ) { stringFromStreamHere( stream, nli->room, sizeof(nli->room) ); stringFromStreamHere( stream, nli->inviteID, sizeof(nli->inviteID) ); - nli->devID = stream_getU32( stream ); + if ( version == 0 ) { + nli->devID = stream_getU32( stream ); + } } if ( types_hasType( nli->_conTypes, COMMS_CONN_BT ) ) { stringFromStreamHere( stream, nli->btName, sizeof(nli->btName) );