Allow grouping of games in collapsible user-defined categores:
+ "Games with Kati", "Finished games", etc.
+
-
Don't try to access directory OS says is for downloads when it
- doesn't actually exist
-
+
Next up
+
+
Improve communication with relay
(The full changelog
diff --git a/xwords4/android/XWords4/res/values/app_name.xml b/xwords4/android/XWords4/res/values/app_name.xml
index 1735f8137..59b7fb0f8 100644
--- a/xwords4/android/XWords4/res/values/app_name.xml
+++ b/xwords4/android/XWords4/res/values/app_name.xml
@@ -1,5 +1,5 @@
- 4.4 beta 54
+ 4.4 beta 58
diff --git a/xwords4/android/XWords4/res/values/common_rsrc.xml b/xwords4/android/XWords4/res/values/common_rsrc.xml
index e09033bba..759d74e00 100644
--- a/xwords4/android/XWords4/res/values/common_rsrc.xml
+++ b/xwords4/android/XWords4/res/values/common_rsrc.xml
@@ -6,6 +6,7 @@
key_color_tileskey_show_arrow
+ key_square_tileskey_explain_robotkey_skip_confirmkey_sort_tiles
@@ -30,7 +31,6 @@
key_clr_bonushintkey_relay_host
- key_redir_hostkey_relay_port2key_update_urlkey_update_prerel
@@ -68,9 +68,10 @@
key_sms_phoneskey_connstat_datakey_dev_id
- key_gcm_regid
+ key_gcmvers_regidkey_relay_regidkey_checked_sms
+ key_default_groupkey_notagain_synckey_notagain_chat
@@ -102,9 +103,12 @@
eehouse.org
-
+ eehouse.org
+ /and/
+ application/x-xwordsinvite
+
+
http://eehouse.org/and_wordlists
- //%1$s/newgame.phpUpdate checks URLhttp://eehouse.org/xw4/info.py
diff --git a/xwords4/android/XWords4/res/values/strings.xml b/xwords4/android/XWords4/res/values/strings.xml
index 7a760b01e..e9288214b 100644
--- a/xwords4/android/XWords4/res/values/strings.xml
+++ b/xwords4/android/XWords4/res/values/strings.xml
@@ -1229,18 +1229,20 @@
encodings for the greater-than and less-than symbols which
are not legal in xml strings.)-->
\u003ca href=\"%1$s\"\u003ETap
- here\u003c/a\u003E (%1$s) to accept my invitation and join this
- game.\u003cbr\u003E \u003ca
- href=\"http://eehouse.org/market_redir.php\"\u003E Tap
- here\u003c/a\u003E (http://eehouse.org/market_redir.php) to
- install Crosswords if you haven\'t already.
+ here\u003c/a\u003E (or tap the full link below, or, if you already
+ have Crosswords installed, open the attachment) to accept my
+ invitation and join this game.
+
+ \u003cbr \\\u003E
+ \u003cbr \\\u003E
+ (full link: %1$s)
+
- Play Crosswords? Join this game: %1$s
- . (But install Crosswords http://eehouse.org/market_redir.php
- first if you haven\'t.)
+ Let\'s play Crosswords! Join this game:
+ %1$s .
- Unable to open game \"%1$s\" because no
- %2$s wordlist found. (It may have been deleted, or stored on
- an external card that is no longer available.)
+ You need to download a replacement %2$s
+ wordlist before you can open game \"%1$s\". (The original may have
+ been deleted or stored on an external card that is no longer
+ available.)You already have a game that seems
- to have been created from the same invitation. Are you sure you
- want to open another?
+ to have been created (on %1$s) from the same invitation. Are you
+ sure you want to create another?
FYI...
@@ -2114,6 +2115,10 @@
play Crosswords using the wordlist %2$s (for play in %3$s), but it
is not installed. Would you like to download the wordlist or
decline the invitation?
+ You have been
+ invited to play Crosswords using the wordlist %2$s (for play in
+ %3$s), but it is not installed. Would you like to download the
+ wordlist?DeclineDownloading %s...
@@ -2124,4 +2129,16 @@
(Not in external/sdcard memory)Downloads Directory
+
+ My games
+ New games
+
+
+ Rematch
+
+ Square rack tiles
+ Even if they can be taller
+
diff --git a/xwords4/android/XWords4/res/xml/xwprefs.xml b/xwords4/android/XWords4/res/xml/xwprefs.xml
index 9a5a3bf3c..2cb350166 100644
--- a/xwords4/android/XWords4/res/xml/xwprefs.xml
+++ b/xwords4/android/XWords4/res/xml/xwprefs.xml
@@ -132,6 +132,11 @@
android:summary="@string/show_arrow_summary"
android:defaultValue="true"
/>
+
+
+
- (width / 7) ) {
+ trayHt = width / 7;
+ }
heightUsed = trayHt + scoreHt + ((nCells - nToScroll) * cellSize);
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/CommsTransport.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/CommsTransport.java
index b6da68897..ba89de474 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/CommsTransport.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/CommsTransport.java
@@ -233,23 +233,21 @@ public class CommsTransport implements TransportProcs,
public void tickle( CommsConnType connType )
{
- m_jniThread.handle( JNIThread.JNICmd.CMD_RESEND, true );
- // CommsAddrRec addr = new CommsAddrRec( m_context );
- // XwJNI.comms_getAddr( m_jniGamePtr, addr );
- // switch( addr.conType ) {
- // case COMMS_CONN_RELAY:
- // // do nothing
- // break;
- // case COMMS_CONN_BT:
- // // Let other know I'm here
- // m_jniThread.handle( JNIThread.JNICmd.CMD_RESEND );
- // break;
- // case COMMS_CONN_SMS:
- // default:
- // DbgUtils.logf( "tickle: unexpected type %s",
- // addr.conType.toString() );
- // Assert.fail();
- // }
+ switch( connType ) {
+ case COMMS_CONN_RELAY:
+ // do nothing
+ // break; // Try skipping the resend -- later
+ case COMMS_CONN_BT:
+ case COMMS_CONN_SMS:
+ // Let other know I'm here
+ DbgUtils.logf( "tickle calling comms_resendAll" );
+ m_jniThread.handle( JNIThread.JNICmd.CMD_RESEND, false, true );
+ break;
+ default:
+ DbgUtils.logf( "tickle: unexpected type %s",
+ connType.toString() );
+ Assert.fail();
+ }
}
private synchronized void putOut( final byte[] buf )
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/ConnStatusHandler.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/ConnStatusHandler.java
index 023b0266d..8ceb78a32 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/ConnStatusHandler.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/ConnStatusHandler.java
@@ -126,7 +126,7 @@ public class ConnStatusHandler {
private static HashMap s_records =
new HashMap();
- private static Object s_lockObj = new Object();
+ private static Class s_lockObj = ConnStatusHandler.class;
private static boolean s_needsSave = false;
public static void setRect( int left, int top, int right, int bottom )
@@ -333,8 +333,8 @@ public class ConnStatusHandler {
try {
ObjectInputStream ois =
new ObjectInputStream( new ByteArrayInputStream(bytes) );
- Object obj = ois.readObject();
- s_records = (HashMap)obj;
+ s_records =
+ (HashMap)ois.readObject();
// } catch ( java.io.StreamCorruptedException sce ) {
// DbgUtils.logf( "loadState: %s", sce.toString() );
// } catch ( java.io.OptionalDataException ode ) {
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java
index 2b905c6b7..cf6a4d6bd 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java
@@ -20,9 +20,10 @@
package org.eehouse.android.xw4;
+import android.content.ContentValues;
import android.content.Context;
-import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
public class DBHelper extends SQLiteOpenHelper {
@@ -30,8 +31,9 @@ public class DBHelper extends SQLiteOpenHelper {
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";
private static final String DB_NAME = "xwdb";
- private static final int DB_VERSION = 14;
+ private static final int DB_VERSION = 15;
public static final String GAME_NAME = "GAME_NAME";
public static final String NUM_MOVES = "NUM_MOVES";
@@ -45,9 +47,6 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String IN_USE = "IN_USE";
public static final String SCORES = "SCORES";
public static final String CHAT_HISTORY = "CHAT_HISTORY";
- // GAMEID: this isn't used yet but we'll want it to look up games
- // for which messages arrive. Add now while changing the DB
- // format
public static final String GAMEID = "GAMEID";
public static final String REMOTEDEVS = "REMOTEDEVS";
public static final String DICTLANG = "DICTLANG";
@@ -61,9 +60,9 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String INVITEID = "INVITEID";
public static final String RELAYID = "RELAYID";
public static final String SEED = "SEED";
- public static final String SMSPHONE = "SMSPHONE";
+ public static final String SMSPHONE = "SMSPHONE"; // unused -- so far
public static final String LASTMOVE = "LASTMOVE";
-
+ public static final String GROUPID = "GROUPID";
public static final String DICTNAME = "DICTNAME";
public static final String MD5SUM = "MD5SUM";
@@ -76,16 +75,79 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String ITERPOS = "ITERPOS";
public static final String ITERTOP = "ITERTOP";
public static final String ITERPREFIX = "ITERPREFIX";
-
- // not used yet
public static final String CREATE_TIME = "CREATE_TIME";
- // not used yet
public static final String LASTPLAY_TIME = "LASTPLAY_TIME";
+ public static final String GROUPNAME = "GROUPNAME";
+ public static final String EXPANDED = "EXPANDED";
+
+ private Context m_context;
+
+ private static final String[] s_summaryColsAndTypes = {
+ GAME_NAME, "TEXT"
+ ,NUM_MOVES, "INTEGER"
+ ,TURN, "INTEGER"
+ ,GIFLAGS, "INTEGER"
+ ,NUM_PLAYERS, "INTEGER"
+ ,MISSINGPLYRS,"INTEGER"
+ ,PLAYERS, "TEXT"
+ ,GAME_OVER, "INTEGER"
+ ,SERVERROLE, "INTEGER"
+ ,CONTYPE, "INTEGER"
+ ,ROOMNAME, "TEXT"
+ ,INVITEID, "TEXT"
+ ,RELAYID, "TEXT"
+ ,SEED, "INTEGER"
+ ,DICTLANG, "INTEGER"
+ ,DICTLIST, "TEXT"
+ ,SMSPHONE, "TEXT" // unused
+ ,SCORES, "TEXT"
+ ,CHAT_HISTORY, "TEXT"
+ ,GAMEID, "INTEGER"
+ ,REMOTEDEVS, "TEXT"
+ ,LASTMOVE, "INTEGER DEFAULT 0"
+ ,GROUPID, "INTEGER"
+ // HASMSGS: sqlite doesn't have bool; use 0 and 1
+ ,HASMSGS, "INTEGER DEFAULT 0"
+ ,CONTRACTED, "INTEGER DEFAULT 0"
+ ,CREATE_TIME, "INTEGER"
+ ,LASTPLAY_TIME,"INTEGER"
+ ,SNAPSHOT, "BLOB"
+ };
+
+ private static final String[] s_obitsColsAndTypes = {
+ RELAYID, "TEXT"
+ ,SEED, "INTEGER"
+ };
+
+ private static final String[] s_dictInfoColsAndTypes = {
+ DICTNAME, "TEXT"
+ ,LOC, "UNSIGNED INTEGER(1)"
+ ,MD5SUM, "TEXT(32)"
+ ,WORDCOUNT,"INTEGER"
+ ,LANGCODE, "INTEGER"
+ };
+
+ private static final String[] s_dictBrowseColsAndTypes = {
+ DICTNAME, "TEXT"
+ ,LOC, "UNSIGNED INTEGER(1)"
+ ,WORDCOUNTS, "TEXT"
+ ,ITERMIN, "INTEGRE(4)"
+ ,ITERMAX, "INTEGER(4)"
+ ,ITERPOS, "INTEGER"
+ ,ITERTOP, "INTEGER"
+ ,ITERPREFIX, "TEXT"
+ };
+
+ private static final String[] s_groupsSchema = {
+ GROUPNAME, "TEXT"
+ ,EXPANDED, "INTEGER(1)"
+ };
public DBHelper( Context context )
{
super( context, DB_NAME, null, DB_VERSION );
+ m_context = context;
}
public static String getDBName()
@@ -93,81 +155,14 @@ public class DBHelper extends SQLiteOpenHelper {
return DB_NAME;
}
- private void onCreateSum( SQLiteDatabase db )
- {
- db.execSQL( "CREATE TABLE " + TABLE_NAME_SUM + " ("
- + GAME_NAME + " TEXT,"
- + NUM_MOVES + " INTEGER,"
- + TURN + " INTEGER,"
- + GIFLAGS + " INTEGER,"
-
- + NUM_PLAYERS + " INTEGER,"
- + MISSINGPLYRS + " INTEGER,"
- + PLAYERS + " TEXT,"
- + GAME_OVER + " INTEGER,"
-
- + SERVERROLE + " INTEGER,"
- + CONTYPE + " INTEGER,"
- + ROOMNAME + " TEXT,"
- + INVITEID + " TEXT,"
- + RELAYID + " TEXT,"
- + SEED + " INTEGER,"
- + DICTLANG + " INTEGER,"
- + DICTLIST + " TEXT,"
-
- + SMSPHONE + " TEXT,"
- + SCORES + " TEXT,"
- + CHAT_HISTORY + " TEXT,"
- + GAMEID + " INTEGER,"
- + REMOTEDEVS + " TEXT,"
- + LASTMOVE + " INTEGER DEFAULT 0,"
- // HASMSGS: sqlite doesn't have bool; use 0 and 1
- + HASMSGS + " INTEGER DEFAULT 0,"
- + CONTRACTED + " INTEGER DEFAULT 0,"
-
- + CREATE_TIME + " INTEGER,"
- + LASTPLAY_TIME + " INTEGER,"
-
- + SNAPSHOT + " BLOB);"
- );
- }
-
- private void onCreateObits( SQLiteDatabase db )
- {
- db.execSQL( "CREATE TABLE " + TABLE_NAME_OBITS + " ("
- + RELAYID + " TEXT,"
- + SEED + " INTEGER);"
- );
- }
-
- private void onCreateDictsDB( SQLiteDatabase db )
- {
- db.execSQL( "CREATE TABLE " + TABLE_NAME_DICTINFO + "("
- + DICTNAME + " TEXT,"
- + LOC + " UNSIGNED INTEGER(1),"
- + MD5SUM + " TEXT(32),"
- + WORDCOUNT + " INTEGER,"
- + LANGCODE + " INTEGER);"
- );
-
- db.execSQL( "CREATE TABLE " + TABLE_NAME_DICTBROWSE + "("
- + DICTNAME + " TEXT,"
- + LOC + " UNSIGNED INTEGER(1),"
- + WORDCOUNTS + " TEXT,"
- + ITERMIN + " INTEGER(4),"
- + ITERMAX + " INTEGER(4),"
- + ITERPOS + " INTEGER,"
- + ITERTOP + " INTEGER,"
- + ITERPREFIX + " TEXT);"
- );
- }
-
@Override
public void onCreate( SQLiteDatabase db )
{
- onCreateSum( db );
- onCreateObits( db );
- onCreateDictsDB( 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 );
+ createGroupsTable( db );
}
@Override
@@ -178,26 +173,30 @@ public class DBHelper extends SQLiteOpenHelper {
switch( oldVersion ) {
case 5:
- onCreateObits(db);
+ createTable( db, TABLE_NAME_OBITS, s_obitsColsAndTypes );
case 6:
- addColumn( db, TURN, "INTEGER" );
- addColumn( db, GIFLAGS, "INTEGER" );
- addColumn( db, CHAT_HISTORY, "TEXT" );
+ addSumColumn( db, TURN );
+ addSumColumn( db, GIFLAGS );
+ addSumColumn( db, CHAT_HISTORY );
case 7:
- addColumn( db, MISSINGPLYRS, "INTEGER" );
+ addSumColumn( db, MISSINGPLYRS );
case 8:
- addColumn( db, GAME_NAME, "TEXT" );
- addColumn( db, CONTRACTED, "INTEGER" );
+ addSumColumn( db, GAME_NAME );
+ addSumColumn( db, CONTRACTED );
case 9:
- addColumn( db, DICTLIST, "TEXT" );
+ addSumColumn( db, DICTLIST );
case 10:
- addColumn( db, INVITEID, "TEXT" );
+ addSumColumn( db, INVITEID );
case 11:
- addColumn( db, REMOTEDEVS, "TEXT" );
+ addSumColumn( db, REMOTEDEVS );
case 12:
- onCreateDictsDB( db );
+ createTable( db, TABLE_NAME_DICTINFO, s_dictInfoColsAndTypes );
+ createTable( db, TABLE_NAME_DICTBROWSE, s_dictBrowseColsAndTypes );
case 13:
- addColumn( db, LASTMOVE, "INTEGER" );
+ addSumColumn( db, LASTMOVE );
+ case 14:
+ addSumColumn( db, GROUPID );
+ createGroupsTable( db );
// nothing yet
break;
default:
@@ -209,10 +208,56 @@ public class DBHelper extends SQLiteOpenHelper {
}
}
- private void addColumn( SQLiteDatabase db, String colName, String colType )
+ private void addSumColumn( SQLiteDatabase db, String colName )
{
+ String colType = null;
+ for ( int ii = 0; ii < s_summaryColsAndTypes.length; ii += 2 ) {
+ if ( s_summaryColsAndTypes[ii].equals( colName ) ) {
+ colType = s_summaryColsAndTypes[ii+1];
+ break;
+ }
+ }
+
String cmd = String.format( "ALTER TABLE %s ADD COLUMN %s %s;",
TABLE_NAME_SUM, colName, colType );
db.execSQL( cmd );
}
+
+ private void createTable( SQLiteDatabase db, String name, String[] data )
+ {
+ StringBuilder query =
+ new StringBuilder( String.format("CREATE TABLE %s (", name ) );
+
+ for ( int ii = 0; ii < data.length; ii += 2 ) {
+ String col = String.format( " %s %s,", data[ii], data[ii+1] );
+ query.append( col );
+ }
+ query.setLength(query.length() - 1); // nuke the last comma
+ query.append( ");" );
+
+ db.execSQL( query.toString() );
+ }
+
+ private void createGroupsTable( SQLiteDatabase db )
+ {
+ createTable( db, TABLE_NAME_GROUPS, s_groupsSchema );
+
+ // Create an empty group name
+ ContentValues values = new ContentValues();
+ values.put( GROUPNAME, m_context.getString(R.string.group_cur_games) );
+ values.put( EXPANDED, 1 );
+ long curGroup = db.insert( TABLE_NAME_GROUPS, null, values );
+ values = new ContentValues();
+ values.put( GROUPNAME, m_context.getString(R.string.group_new_games) );
+ values.put( EXPANDED, 0 );
+ long newGroup = db.insert( TABLE_NAME_GROUPS, null, 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 );
+
+ XWPrefs.setDefaultNewGameGroup( m_context, newGroup );
+ }
+
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBUtils.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBUtils.java
index 48055402f..84a8bcae5 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBUtils.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBUtils.java
@@ -60,9 +60,10 @@ public class DBUtils {
private static long s_cachedRowID = -1;
private static byte[] s_cachedBytes = null;
+ private static long[] s_cachedRowIDs = null;
public static interface DBChangeListener {
- public void gameSaved( long rowid );
+ public void gameSaved( long rowid, boolean countChanged );
}
private static HashSet s_listeners =
new HashSet();
@@ -100,8 +101,7 @@ public class DBUtils {
long maxMillis )
{
GameSummary result = null;
- GameUtils.GameLock lock =
- new GameUtils.GameLock( rowid, false ).lock( maxMillis );
+ GameLock lock = new GameLock( rowid, false ).lock( maxMillis );
if ( null != lock ) {
result = getSummary( context, lock );
lock.unlock();
@@ -115,7 +115,7 @@ public class DBUtils {
}
public static GameSummary getSummary( Context context,
- GameUtils.GameLock lock )
+ GameLock lock )
{
initDB( context );
GameSummary summary = null;
@@ -129,7 +129,7 @@ public class DBUtils {
DBHelper.TURN, DBHelper.GIFLAGS,
DBHelper.CONTYPE, DBHelper.SERVERROLE,
DBHelper.ROOMNAME, DBHelper.RELAYID,
- DBHelper.SMSPHONE, DBHelper.SEED,
+ /*DBHelper.SMSPHONE,*/ DBHelper.SEED,
DBHelper.DICTLANG, DBHelper.GAMEID,
DBHelper.SCORES, DBHelper.HASMSGS,
DBHelper.LASTPLAY_TIME, DBHelper.REMOTEDEVS,
@@ -247,13 +247,13 @@ public class DBUtils {
return summary;
} // getSummary
- public static void saveSummary( Context context, GameUtils.GameLock lock,
+ public static void saveSummary( Context context, GameLock lock,
GameSummary summary )
{
saveSummary( context, lock, summary, null );
}
- public static void saveSummary( Context context, GameUtils.GameLock lock,
+ public static void saveSummary( Context context, GameLock lock,
GameSummary summary, String inviteID )
{
Assert.assertTrue( lock.canWrite() );
@@ -314,9 +314,12 @@ public class DBUtils {
long result = db.update( DBHelper.TABLE_NAME_SUM,
values, selection, null );
Assert.assertTrue( result >= 0 );
+ if ( result != rowid ) { // new row added
+ clearRowIDsCache();
+ }
}
- notifyListeners( rowid );
db.close();
+ notifyListeners( rowid, false );
}
} // saveSummary
@@ -364,24 +367,15 @@ public class DBUtils {
private static void setInt( long rowid, String column, int value )
{
- synchronized( s_dbHelper ) {
- SQLiteDatabase db = s_dbHelper.getWritableDatabase();
-
- String selection = String.format( ROW_ID_FMT, rowid );
- ContentValues values = new ContentValues();
- values.put( column, value );
-
- int result = db.update( DBHelper.TABLE_NAME_SUM,
- values, selection, null );
- Assert.assertTrue( result == 1 );
- db.close();
- }
+ ContentValues values = new ContentValues();
+ values.put( column, value );
+ updateRow( null, DBHelper.TABLE_NAME_SUM, rowid, values );
}
public static void setMsgFlags( long rowid, int flags )
{
setInt( rowid, DBHelper.HASMSGS, flags );
- notifyListeners( rowid );
+ notifyListeners( rowid, false );
}
public static void setExpanded( long rowid, boolean expanded )
@@ -456,10 +450,8 @@ public class DBUtils {
String selection = DBHelper.RELAYID + "='" + relayID + "'";
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, null, null, null, null );
+ result = new long[cursor.getCount()];
for ( int ii = 0; cursor.moveToNext(); ++ii ) {
- if ( null == result ) {
- result = new long[cursor.getCount()];
- }
result[ii] = cursor.getLong( cursor.getColumnIndex(ROW_ID) );
}
cursor.close();
@@ -478,11 +470,8 @@ public class DBUtils {
String selection = String.format( DBHelper.GAMEID + "=%d", gameID );
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, null, null, null, null );
-
+ result = new long[cursor.getCount()];
for ( int ii = 0; cursor.moveToNext(); ++ii ) {
- if ( null == result ) {
- result = new long[cursor.getCount()];
- }
result[ii] = cursor.getLong( cursor.getColumnIndex(ROW_ID) );
}
cursor.close();
@@ -544,21 +533,28 @@ public class DBUtils {
}
}
- public static long getRowIDForOpen( Context context, NetLaunchInfo nli )
+ // Return creation time of newest game matching this nli, or null
+ // if none found.
+ public static Date getMostRecentCreate( Context context,
+ NetLaunchInfo nli )
{
- long result = ROWID_NOTFOUND;
+ Date result = null;
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getReadableDatabase();
- String[] columns = { ROW_ID };
- String selection = DBHelper.ROOMNAME + "='" + nli.room + "' AND "
- + DBHelper.INVITEID + "='" + nli.inviteID + "' AND "
- + DBHelper.DICTLANG + "=" + nli.lang + " AND "
- + DBHelper.NUM_PLAYERS + "=" + nli.nPlayers;
+ String[] columns = { DBHelper.CREATE_TIME };
+ String selection =
+ String.format( "%s='%s' AND %s='%s' AND %s=%d AND %s=%d",
+ DBHelper.ROOMNAME, nli.room,
+ DBHelper.INVITEID, nli.inviteID,
+ DBHelper.DICTLANG, nli.lang,
+ DBHelper.NUM_PLAYERS, nli.nPlayersT );
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
- selection, null, null, null, null );
- if ( 1 == cursor.getCount() && cursor.moveToFirst() ) {
- result = cursor.getLong( cursor.getColumnIndex(ROW_ID) );
+ selection, null, null, null,
+ DBHelper.CREATE_TIME + " DESC" ); // order by
+ if ( cursor.moveToNext() ) {
+ int indx = cursor.getColumnIndex( DBHelper.CREATE_TIME );
+ result = new Date( cursor.getLong( indx ) );
}
cursor.close();
db.close();
@@ -566,13 +562,17 @@ public class DBUtils {
return result;
}
- public static boolean isNewInvite( Context context, Uri data )
+ public static Date getMostRecentCreate( Context context, Uri data )
{
- NetLaunchInfo nli = new NetLaunchInfo( data );
- return null != nli && -1 == getRowIDForOpen( context, nli );
+ Date result = null;
+ NetLaunchInfo nli = new NetLaunchInfo( context, data );
+ if ( null != nli && nli.isValid() ) {
+ result = getMostRecentCreate( context, nli );
+ }
+ return result;
}
- public static String[] getRelayIDs( Context context, boolean noMsgs )
+ public static String[] getRelayIDs( Context context, long[][] rowIDs )
{
String[] result = null;
initDB( context );
@@ -580,26 +580,31 @@ public class DBUtils {
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getReadableDatabase();
- String[] columns = { DBHelper.RELAYID };
+ String[] columns = { ROW_ID, DBHelper.RELAYID };
String selection = DBHelper.RELAYID + " NOT null";
- if ( noMsgs ) {
- selection += " AND NOT " + DBHelper.HASMSGS;
- }
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, null, null, null, null );
+ int count = cursor.getCount();
+ if ( 0 < count ) {
+ result = new String[count];
+ if ( null != rowIDs ) {
+ rowIDs[0] = new long[count];
+ }
- while ( cursor.moveToNext() ) {
- ids.add( cursor.getString( cursor.
- getColumnIndex(DBHelper.RELAYID)) );
+ int idIndex = cursor.getColumnIndex(DBHelper.RELAYID);
+ int rowIndex = cursor.getColumnIndex(ROW_ID);
+ for ( int ii = 0; cursor.moveToNext(); ++ii ) {
+ result[ii] = cursor.getString( idIndex );
+ if ( null != rowIDs ) {
+ rowIDs[0][ii] = cursor.getLong( rowIndex );
+ }
+ }
}
cursor.close();
db.close();
}
- if ( 0 < ids.size() ) {
- result = ids.toArray( new String[ids.size()] );
- }
return result;
}
@@ -673,9 +678,9 @@ public class DBUtils {
}
}
- public static GameUtils.GameLock saveNewGame( Context context, byte[] bytes )
+ public static GameLock saveNewGame( Context context, byte[] bytes )
{
- GameUtils.GameLock lock = null;
+ GameLock lock = null;
initDB( context );
synchronized( s_dbHelper ) {
@@ -687,58 +692,46 @@ public class DBUtils {
long timestamp = new Date().getTime();
values.put( DBHelper.CREATE_TIME, timestamp );
values.put( DBHelper.LASTPLAY_TIME, timestamp );
+ values.put( DBHelper.GROUPID,
+ XWPrefs.getDefaultNewGameGroup( context ) );
long rowid = db.insert( DBHelper.TABLE_NAME_SUM, null, values );
setCached( rowid, null ); // force reread
+ clearRowIDsCache();
- lock = new GameUtils.GameLock( rowid, true ).lock();
-
- notifyListeners( rowid );
+ lock = new GameLock( rowid, true ).lock();
+ notifyListeners( rowid, true );
}
return lock;
}
- public static long saveGame( Context context, GameUtils.GameLock lock,
+ public static long saveGame( Context context, GameLock lock,
byte[] bytes, boolean setCreate )
{
Assert.assertTrue( lock.canWrite() );
long rowid = lock.getRowid();
- initDB( context );
- synchronized( s_dbHelper ) {
- SQLiteDatabase db = s_dbHelper.getWritableDatabase();
- String selection = String.format( ROW_ID_FMT, rowid );
- ContentValues values = new ContentValues();
- values.put( DBHelper.SNAPSHOT, bytes );
+ ContentValues values = new ContentValues();
+ values.put( DBHelper.SNAPSHOT, bytes );
- long timestamp = new Date().getTime();
- if ( setCreate ) {
- values.put( DBHelper.CREATE_TIME, timestamp );
- }
- values.put( DBHelper.LASTPLAY_TIME, timestamp );
-
- int result = db.update( DBHelper.TABLE_NAME_SUM,
- values, selection, null );
- if ( 0 == result ) {
- Assert.fail();
- // values.put( DBHelper.FILE_NAME, path );
- // rowid = db.insert( DBHelper.TABLE_NAME_SUM, null, values );
- // DbgUtils.logf( "insert=>%d", rowid );
- // Assert.assertTrue( row >= 0 );
- }
- db.close();
+ long timestamp = new Date().getTime();
+ if ( setCreate ) {
+ values.put( DBHelper.CREATE_TIME, timestamp );
}
- setCached( rowid, null ); // force reread
+ values.put( DBHelper.LASTPLAY_TIME, timestamp );
- if ( -1 != rowid ) {
- notifyListeners( rowid );
+ updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
+
+ setCached( rowid, null ); // force reread
+ if ( -1 != rowid ) { // Means new game?
+ notifyListeners( rowid, false );
}
return rowid;
}
- public static byte[] loadGame( Context context, GameUtils.GameLock lock )
+ public static byte[] loadGame( Context context, GameLock lock )
{
long rowid = lock.getRowid();
Assert.assertTrue( -1 != rowid );
@@ -766,12 +759,16 @@ public class DBUtils {
public static void deleteGame( Context context, long rowid )
{
- GameUtils.GameLock lock = new GameUtils.GameLock( rowid, true ).lock();
- deleteGame( context, lock );
- lock.unlock();
+ GameLock lock = new GameLock( rowid, true ).lock( 300 );
+ if ( null != lock ) {
+ deleteGame( context, lock );
+ lock.unlock();
+ } else {
+ DbgUtils.logf( "deleteGame: unable to lock rowid %d", rowid );
+ }
}
- public static void deleteGame( Context context, GameUtils.GameLock lock )
+ public static void deleteGame( Context context, GameLock lock )
{
Assert.assertTrue( lock.canWrite() );
initDB( context );
@@ -781,34 +778,46 @@ public class DBUtils {
db.delete( DBHelper.TABLE_NAME_SUM, selection, null );
db.close();
}
- notifyListeners( lock.getRowid() );
+ clearRowIDsCache();
+ notifyListeners( lock.getRowid(), true );
}
public static long[] gamesList( Context context )
{
- long[] result = null;
+ long[] result;
+ synchronized( DBUtils.class ) {
+ if ( null == s_cachedRowIDs ) {
+ initDB( context );
+ synchronized( s_dbHelper ) {
+ SQLiteDatabase db = s_dbHelper.getReadableDatabase();
- initDB( context );
- synchronized( s_dbHelper ) {
- SQLiteDatabase db = s_dbHelper.getReadableDatabase();
-
- String[] columns = { ROW_ID };
- String orderBy = DBHelper.CREATE_TIME + " DESC";
- Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
- null, null, null, null, orderBy );
- int count = cursor.getCount();
- result = new long[count];
- int index = cursor.getColumnIndex( ROW_ID );
- for ( int ii = 0; cursor.moveToNext(); ++ii ) {
- result[ii] = cursor.getLong( index );
+ String[] columns = { ROW_ID };
+ String orderBy = DBHelper.CREATE_TIME + " DESC";
+ Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM,
+ columns, null, null, null,
+ null, orderBy );
+ int count = cursor.getCount();
+ s_cachedRowIDs = new long[count];
+ int index = cursor.getColumnIndex( ROW_ID );
+ for ( int ii = 0; cursor.moveToNext(); ++ii ) {
+ s_cachedRowIDs[ii] = cursor.getLong( index );
+ }
+ cursor.close();
+ db.close();
+ }
}
- cursor.close();
- db.close();
+ result = s_cachedRowIDs;
}
-
return result;
}
+ private static void clearRowIDsCache()
+ {
+ synchronized( DBUtils.class ) {
+ s_cachedRowIDs = null;
+ }
+ }
+
// Get either the file name or game name, preferring the latter.
public static String getName( Context context, long rowid )
{
@@ -834,21 +843,9 @@ public class DBUtils {
public static void setName( Context context, long rowid, String name )
{
- initDB( context );
- synchronized( s_dbHelper ) {
- SQLiteDatabase db = s_dbHelper.getWritableDatabase();
-
- String selection = String.format( ROW_ID_FMT, rowid );
- ContentValues values = new ContentValues();
- values.put( DBHelper.GAME_NAME, name );
-
- int result = db.update( DBHelper.TABLE_NAME_SUM,
- values, selection, null );
- db.close();
- if ( 0 == result ) {
- DbgUtils.logf( "setName(%d,%s) failed", rowid, name );
- }
- }
+ ContentValues values = new ContentValues();
+ values.put( DBHelper.GAME_NAME, name );
+ updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
}
public static HistoryPair[] getChatHistory( Context context, long rowid )
@@ -928,6 +925,7 @@ public class DBUtils {
public static void loadDB( Context context )
{
+ clearRowIDsCache();
copyGameDB( context, false );
}
@@ -1190,42 +1188,49 @@ public class DBUtils {
private static void saveChatHistory( Context context, long rowid,
String history )
{
- initDB( context );
- synchronized( s_dbHelper ) {
- SQLiteDatabase db = s_dbHelper.getWritableDatabase();
-
- String selection = String.format( ROW_ID_FMT, rowid );
- ContentValues values = new ContentValues();
- if ( null != history ) {
- values.put( DBHelper.CHAT_HISTORY, history );
- } else {
- values.putNull( DBHelper.CHAT_HISTORY );
- }
-
- long timestamp = new Date().getTime();
- values.put( DBHelper.LASTPLAY_TIME, timestamp );
-
- int result = db.update( DBHelper.TABLE_NAME_SUM,
- values, selection, null );
- db.close();
+ ContentValues values = new ContentValues();
+ if ( null != history ) {
+ values.put( DBHelper.CHAT_HISTORY, history );
+ } else {
+ values.putNull( DBHelper.CHAT_HISTORY );
}
+ values.put( DBHelper.LASTPLAY_TIME, new Date().getTime() );
+ updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
}
private static void initDB( Context context )
{
if ( null == s_dbHelper ) {
+ Assert.assertNotNull( context );
s_dbHelper = new DBHelper( context );
// force any upgrade
s_dbHelper.getWritableDatabase().close();
}
}
- private static void notifyListeners( long rowid )
+ private static void updateRow( Context context, String table,
+ long rowid, ContentValues values )
+ {
+ initDB( context );
+ synchronized( s_dbHelper ) {
+ SQLiteDatabase db = s_dbHelper.getWritableDatabase();
+
+ String selection = String.format( ROW_ID_FMT, rowid );
+
+ int result = db.update( table, values, selection, null );
+ db.close();
+ if ( 0 == result ) {
+ DbgUtils.logf( "updateRow failed" );
+ }
+ }
+ }
+
+ private static void notifyListeners( long rowid, boolean countChanged )
{
synchronized( s_listeners ) {
Iterator iter = s_listeners.iterator();
while ( iter.hasNext() ) {
- iter.next().gameSaved( rowid );
+ iter.next().gameSaved( rowid, countChanged );
}
}
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictImportActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictImportActivity.java
index cb2cdd638..6b3e0e20d 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictImportActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictImportActivity.java
@@ -1,7 +1,7 @@
/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
/*
- * Copyright 2009-2010 by Eric House (xwords@eehouse.org). All
- * rights reserved.
+ * Copyright 2009-2012 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
@@ -21,31 +21,72 @@
package org.eehouse.android.xw4;
import android.app.Activity;
-import android.os.Bundle;
-import android.os.AsyncTask;
+import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
import android.view.Window;
import android.widget.ProgressBar;
import android.widget.TextView;
-import java.io.InputStream;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
import java.net.URI;
+import java.security.MessageDigest;
+import java.util.HashMap;
import junit.framework.Assert;
public class DictImportActivity extends XWActivity {
+ // URIs coming in in intents
+ private static final String APK_EXTRA = "APK";
+ private static final String DICT_EXTRA = "XWD";
+
+ public interface DownloadFinishedListener {
+ void downloadFinished( String name, boolean success );
+ }
+
+ // Track callbacks for downloads.
+ private static class ListenerData {
+ public ListenerData( String dictName, DownloadFinishedListener lstnr )
+ {
+ m_dictName = dictName;
+ m_lstnr = lstnr;
+ }
+ public String m_dictName;
+ public DownloadFinishedListener m_lstnr;
+ }
+ private static HashMap s_listeners =
+ new HashMap();
+
private class DownloadFilesTask extends AsyncTask {
- private String m_saved = null;
+ private String m_savedDict = null;
+ private String m_url = null;
+ private boolean m_isApp = false;
+ private File m_appFile = null;
+
+ public DownloadFilesTask( boolean isApp )
+ {
+ super();
+ m_isApp = isApp;
+ }
+
+ public DownloadFilesTask( String url, boolean isApp )
+ {
+ this( isApp );
+ m_url = url;
+ }
+
@Override
protected Long doInBackground( Uri... uris )
{
- m_saved = null;
+ m_savedDict = null;
+ m_appFile = null;
int count = uris.length;
Assert.assertTrue( 1 == count );
- long totalSize = 0;
for ( int ii = 0; ii < count; ii++ ) {
Uri uri = uris[ii];
DbgUtils.logf( "trying %s", uri );
@@ -55,7 +96,12 @@ public class DictImportActivity extends XWActivity {
uri.getSchemeSpecificPart(),
uri.getFragment() );
InputStream is = jUri.toURL().openStream();
- m_saved = saveDict( is, uri.getPath() );
+ String name = basename( uri.getPath() );
+ if ( m_isApp ) {
+ m_appFile = saveToDownloads( is, name );
+ } else {
+ m_savedDict = saveDict( is, name );
+ }
is.close();
} catch ( java.net.URISyntaxException use ) {
DbgUtils.loge( use );
@@ -65,18 +111,26 @@ public class DictImportActivity extends XWActivity {
DbgUtils.loge( ioe );
}
}
- return totalSize;
+ return new Long(0);
}
@Override
protected void onPostExecute( Long result )
{
DbgUtils.logf( "onPostExecute passed %d", result );
- if ( null != m_saved ) {
+ if ( null != m_savedDict ) {
DictUtils.DictLoc loc =
XWPrefs.getDefaultLoc( DictImportActivity.this );
- DictLangCache.inval( DictImportActivity.this, m_saved,
+ DictLangCache.inval( DictImportActivity.this, m_savedDict,
loc, true );
+ callListener( m_url, true );
+ } else if ( null != m_appFile ) {
+ // launch the installer
+ Intent intent = Utils.makeInstallIntent( m_appFile );
+ startActivity( intent );
+ } else {
+ // we failed at something....
+ callListener( m_url, false );
}
finish();
}
@@ -86,6 +140,7 @@ public class DictImportActivity extends XWActivity {
protected void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
+ DownloadFilesTask dft = null;
requestWindowFeature( Window.FEATURE_LEFT_ICON );
setContentView( R.layout.import_dict );
@@ -96,27 +151,64 @@ public class DictImportActivity extends XWActivity {
Intent intent = getIntent();
Uri uri = intent.getData();
- if ( null != uri) {
- if ( null != intent.getType()
- && intent.getType().equals( "application/x-xwordsdict" ) ) {
- DbgUtils.logf( "based on MIME type" );
- new DownloadFilesTask().execute( uri );
- } else if ( uri.toString().endsWith( XWConstants.DICT_EXTN ) ) {
- String txt = getString( R.string.downloading_dictf,
- basename( uri.getPath()) );
- TextView view = (TextView)findViewById( R.id.dwnld_message );
- view.setText( txt );
- new DownloadFilesTask().execute( uri );
- } else {
- DbgUtils.logf( "bogus intent: %s/%s", intent.getType(), uri );
- finish();
- }
+ if ( null == uri ) {
+ String url = intent.getStringExtra( APK_EXTRA );
+ boolean isApp = null != url;
+ if ( !isApp ) {
+ url = intent.getStringExtra( DICT_EXTRA );
+ }
+ if ( null != url ) {
+ dft = new DownloadFilesTask( url, isApp );
+ uri = Uri.parse( url );
+ }
+ } else if ( null != intent.getType()
+ && intent.getType().equals( "application/x-xwordsdict" ) ) {
+ dft = new DownloadFilesTask( false );
+ } else if ( uri.toString().endsWith( XWConstants.DICT_EXTN ) ) {
+ dft = new DownloadFilesTask( uri.toString(), false );
+ }
+
+ if ( null == dft ) {
+ finish();
+ } else {
+ String showName = basename( uri.getPath() );
+ String msg = getString( R.string.downloading_dictf, showName );
+ TextView view = (TextView)findViewById( R.id.dwnld_message );
+ view.setText( msg );
+
+ dft.execute( uri );
}
}
- private String saveDict( InputStream inputStream, String path )
+ private File saveToDownloads( InputStream is, String name )
+ {
+ boolean success = false;
+ File appFile = new File( DictUtils.getDownloadDir( this ), name );
+
+ byte[] buf = new byte[1024*4];
+ try {
+ FileOutputStream fos = new FileOutputStream( appFile );
+ int nRead;
+ while ( 0 <= (nRead = is.read( buf, 0, buf.length )) ) {
+ fos.write( buf, 0, nRead );
+ }
+ fos.close();
+ success = true;
+ } catch ( java.io.FileNotFoundException fnf ) {
+ DbgUtils.loge( fnf );
+ } catch ( java.io.IOException ioe ) {
+ DbgUtils.loge( ioe );
+ }
+
+ if ( !success ) {
+ appFile.delete();
+ appFile = null;
+ }
+ return appFile;
+ }
+
+ private String saveDict( InputStream inputStream, String name )
{
- String name = basename( path );
DictUtils.DictLoc loc = XWPrefs.getDefaultLoc( this );
if ( !DictUtils.saveDict( this, inputStream, name, loc ) ) {
name = null;
@@ -128,6 +220,57 @@ public class DictImportActivity extends XWActivity {
{
return new File(path).getName();
}
+
+ private static void rememberListener( String url, String name,
+ DownloadFinishedListener lstnr )
+ {
+ ListenerData ld = new ListenerData( name, lstnr );
+ synchronized( s_listeners ) {
+ s_listeners.put( url, ld );
+ }
+ }
+
+ private static void callListener( String url, boolean success )
+ {
+ if ( null != url ) {
+ ListenerData ld;
+ synchronized( s_listeners ) {
+ ld = s_listeners.get( url );
+ if ( null != ld ) {
+ s_listeners.remove( url );
+ }
+ }
+ if ( null != ld ) {
+ ld.m_lstnr.downloadFinished( ld.m_dictName, success );
+ }
+ }
+ }
+
+ public static void downloadDictInBack( Context context, int lang,
+ String name,
+ DownloadFinishedListener lstnr )
+ {
+ String url = Utils.makeDictUrl( context, lang, name );
+ if ( null != lstnr ) {
+ rememberListener( url, name, lstnr );
+ }
+ downloadDictInBack( context, url );
+ }
+
+ public static void downloadDictInBack( Context context, String url )
+ {
+ Intent intent = new Intent( context, DictImportActivity.class );
+ intent.putExtra( DICT_EXTRA, url );
+ context.startActivity( intent );
+ }
+
+ public static Intent makeAppDownloadIntent( Context context, String url )
+ {
+ Intent intent = new Intent( context, DictImportActivity.class );
+ intent.putExtra( APK_EXTRA, url );
+ return intent;
+ }
+
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictUtils.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictUtils.java
index bfef92453..05fff08d0 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictUtils.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictUtils.java
@@ -47,6 +47,20 @@ import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
public class DictUtils {
+ // Standard hack for using APIs from an SDK in code to ship on
+ // older devices that don't support it: prevent class loader from
+ // seeing something it'll barf on by loading it manually
+ private static interface SafeDirGetter {
+ public File getDownloadDir();
+ }
+ private static SafeDirGetter s_dirGetter = null;
+ static {
+ int sdkVersion = Integer.valueOf( android.os.Build.VERSION.SDK );
+ if ( 8 <= sdkVersion ) {
+ s_dirGetter = new DirGetter();
+ }
+ }
+
// keep in sync with loc_names string-array
public enum DictLoc { UNKNOWN, BUILT_IN, INTERNAL, EXTERNAL, DOWNLOAD };
public static final String INVITED = "invited";
@@ -566,22 +580,45 @@ public class DictUtils {
return null != getDownloadDir( context );
}
- private static File getDownloadDir( Context context )
+ // Loop through three ways of getting the directory until one
+ // produces a directory I can write to.
+ public static File getDownloadDir( Context context )
{
File result = null;
- if ( haveWriteableSD() ) {
- File file = null;
- String myPath = XWPrefs.getMyDownloadDir( context );
- if ( null != myPath && 0 < myPath.length() ) {
- file = new File( myPath );
- } else {
- file = Environment.getExternalStorageDirectory();
- if ( null != file ) {
- file = new File( file, "download/" );
+ outer:
+ for ( int attempt = 0; attempt < 4; ++attempt ) {
+ switch ( attempt ) {
+ case 0:
+ String myPath = XWPrefs.getMyDownloadDir( context );
+ if ( null == myPath || 0 == myPath.length() ) {
+ continue;
}
+ result = new File( myPath );
+ break;
+ case 1:
+ if ( null == s_dirGetter ) {
+ continue;
+ }
+ result = s_dirGetter.getDownloadDir();
+ break;
+ case 2:
+ case 3:
+ if ( !haveWriteableSD() ) {
+ continue;
+ }
+ result = Environment.getExternalStorageDirectory();
+ if ( 2 == attempt && null != result ) {
+ // the old way...
+ result = new File( result, "download/" );
+ }
+ break;
}
- if ( null != file && file.exists() && file.isDirectory() ) {
- result = file;
+
+ // Exit test for loop
+ if ( null != result ) {
+ if ( result.exists() && result.isDirectory() && result.canWrite() ) {
+ break outer;
+ }
}
}
return result;
@@ -596,4 +633,13 @@ public class DictUtils {
}
return result;
}
+
+ private static class DirGetter implements SafeDirGetter {
+ public File getDownloadDir()
+ {
+ File path = Environment.
+ getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
+ return path;
+ }
+ }
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictsActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictsActivity.java
index e56b37559..b3f3331ff 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictsActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictsActivity.java
@@ -61,7 +61,7 @@ import org.eehouse.android.xw4.DictUtils.DictLoc;
public class DictsActivity extends ExpandableListActivity
implements View.OnClickListener, XWListItem.DeleteCallback,
MountEventReceiver.SDCardNotifiee, DlgDelegate.DlgClickNotify,
- NetUtils.DownloadFinishedListener {
+ DictImportActivity.DownloadFinishedListener {
private static final String DICT_DOLAUNCH = "do_launch";
private static final String DICT_LANG_EXTRA = "use_lang";
@@ -339,8 +339,9 @@ public class DictsActivity extends ExpandableListActivity
String name = intent.getStringExtra( MultiService.DICT );
m_launchedForMissing = true;
m_handler = new Handler();
- NetUtils.downloadDictInBack( DictsActivity.this, lang,
- name, DictsActivity.this );
+ DictImportActivity
+ .downloadDictInBack( DictsActivity.this, lang,
+ name, DictsActivity.this );
}
};
lstnr2 = new OnClickListener() {
@@ -555,10 +556,9 @@ public class DictsActivity extends ExpandableListActivity
{
int loci = intent.getIntExtra( UpdateCheckReceiver.NEW_DICT_LOC, 0 );
if ( 0 < loci ) {
- DictLoc loc = DictLoc.values()[loci];
String url =
intent.getStringExtra( UpdateCheckReceiver.NEW_DICT_URL );
- NetUtils.downloadDictInBack( this, url, loc, null );
+ DictImportActivity.downloadDictInBack( this, url );
finish();
}
}
@@ -769,7 +769,7 @@ public class DictsActivity extends ExpandableListActivity
launchAndDownload( activity, 0, null );
}
- // NetUtils.DownloadFinishedListener interface
+ // DictImportActivity.DownloadFinishedListener interface
public void downloadFinished( String name, final boolean success )
{
if ( m_launchedForMissing ) {
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DispatchNotify.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DispatchNotify.java
index ab9231174..c2178a99b 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DispatchNotify.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DispatchNotify.java
@@ -29,145 +29,20 @@ import android.os.Bundle;
import java.util.HashSet;
import junit.framework.Assert;
+import org.eehouse.android.xw4.jni.GameSummary;
+
public class DispatchNotify extends Activity {
- public static final String RELAYIDS_EXTRA = "relayids";
- public static final String GAMEID_EXTRA = "gameid";
-
- public interface HandleRelaysIface {
- void handleRelaysIDs( final String[] relayIDs );
- void handleInvite( final Uri invite );
- void handleGameID( int gameID );
- }
-
- private static HashSet s_running =
- new HashSet();
- private static HandleRelaysIface s_handler;
-
@Override
protected void onCreate( Bundle savedInstanceState )
{
- boolean mustLaunch = false;
super.onCreate( savedInstanceState );
- String[] relayIDs = getIntent().getStringArrayExtra( RELAYIDS_EXTRA );
- int gameID = getIntent().getIntExtra( GAMEID_EXTRA, -1 );
Uri data = getIntent().getData();
-
- if ( null != relayIDs ) {
- if ( !tryHandle( relayIDs ) ) {
- mustLaunch = true;
- }
- } else if ( -1 != gameID ) {
- if ( !tryHandle( gameID ) ) {
- mustLaunch = true;
- }
- } else if ( null != data ) {
- if ( DBUtils.isNewInvite( this, data ) ) {
- if ( !tryHandle( data ) ) {
- mustLaunch = true;
- }
- } else {
- DbgUtils.logf( "DispatchNotify: dropping duplicate invite" );
- }
- }
-
- if ( mustLaunch ) {
- DbgUtils.logf( "DispatchNotify: nothing running" );
- Intent intent = new Intent( this, GamesList.class );
-
- // This combination of flags will bring an existing
- // GamesList instance to the front, killing any children
- // it has, or create a new one if none exists. Coupled
- // with a "standard" launchMode it seems to work, meaning
- // both that the app preserves its stack in normal use
- // (you can go to Home with a stack of activities and
- // return to the top activity on that stack if you
- // relaunch the app) and that when I launch from here the
- // stack gets nuked and we don't get a second GamesList
- // instance.
-
- intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP
- | Intent.FLAG_ACTIVITY_NEW_TASK );
- if ( null != relayIDs ) {
- intent.putExtra( RELAYIDS_EXTRA, relayIDs );
- } else if ( -1 != gameID ) {
- intent.putExtra( GAMEID_EXTRA, gameID );
- } else if ( null != data ) {
- intent.setData( data );
- } else {
- Assert.fail();
- }
- startActivity( intent );
+ if ( null != data ) { // relay invite redirected URL case
+ GamesList.openGame( this, data );
}
finish();
- }
-
- public static void SetRunning( Activity running )
- {
- if ( running instanceof HandleRelaysIface ) {
- s_running.add( (HandleRelaysIface)running );
- }
- }
-
- public static void ClearRunning( Activity running )
- {
- if ( running instanceof HandleRelaysIface ) {
- s_running.remove( (HandleRelaysIface)running );
- }
- }
-
- public static void SetRelayIDsHandler( HandleRelaysIface iface )
- {
- s_handler = iface;
- }
-
- private static boolean tryHandle( Uri data )
- {
- boolean handled = false;
- if ( null != s_handler ) {
- // This means the GamesList activity is frontmost
- s_handler.handleInvite( data );
- handled = true;
- } else {
- for ( HandleRelaysIface iface : s_running ) {
- iface.handleInvite( data );
- handled = true;
- }
- }
- return handled;
- }
-
- public static boolean tryHandle( String[] relayIDs )
- {
- boolean handled = false;
- if ( null != s_handler ) {
- // This means the GamesList activity is frontmost
- s_handler.handleRelaysIDs( relayIDs );
- handled = true;
- } else {
- for ( HandleRelaysIface iface : s_running ) {
- iface.handleRelaysIDs( relayIDs );
- handled = true;
- }
- }
- return handled;
- }
-
- public static boolean tryHandle( int gameID )
- {
- boolean handled = false;
- if ( null != s_handler ) {
- // This means the GamesList activity is frontmost
- s_handler.handleGameID( gameID );
- handled = true;
- } else {
- for ( HandleRelaysIface iface : s_running ) {
- iface.handleGameID( gameID );
- handled = true;
- }
- }
- return handled;
- }
+ } // onCreate
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DlgDelegate.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DlgDelegate.java
index 76d4e55ef..5772b14ba 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DlgDelegate.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DlgDelegate.java
@@ -251,7 +251,7 @@ public class DlgDelegate {
public void doSyncMenuitem()
{
- if ( null == DBUtils.getRelayIDs( m_activity, false ) ) {
+ if ( null == DBUtils.getRelayIDs( m_activity, null ) ) {
showOKOnlyDialog( R.string.no_games_to_refresh );
} else {
RelayReceiver.RestartTimer( m_activity, true );
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/ExpiringDelegate.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/ExpiringDelegate.java
index 7d1b13968..e1cedab05 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/ExpiringDelegate.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/ExpiringDelegate.java
@@ -194,12 +194,20 @@ public class ExpiringDelegate {
if ( null == m_runnable ) {
m_runnable = new Runnable() {
public void run() {
+ if ( XWApp.DEBUG_EXP_TIMERS ) {
+ DbgUtils.logf( "ExpiringDelegate: timer fired"
+ + " for %H", this );
+ }
if ( m_active ) {
figurePct();
if ( m_haveTurnLocal ) {
m_back = null;
setBackground();
}
+ if ( XWApp.DEBUG_EXP_TIMERS ) {
+ DbgUtils.logf( "ExpiringDelegate: invalidating"
+ + " view %H", m_view );
+ }
m_view.invalidate();
}
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GCMIntentService.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GCMIntentService.java
index e9eb0e995..532e70329 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GCMIntentService.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GCMIntentService.java
@@ -28,6 +28,11 @@ import com.google.android.gcm.GCMRegistrar;
public class GCMIntentService extends GCMBaseIntentService {
+ public GCMIntentService()
+ {
+ super( GCMConsts.SENDER_ID );
+ }
+
@Override
protected void onError( Context context, String error )
{
@@ -37,12 +42,12 @@ public class GCMIntentService extends GCMBaseIntentService {
@Override
protected void onRegistered( Context context, String regId )
{
- DbgUtils.logf("GCMIntentService.onRegistered(%s)", regId );
+ DbgUtils.logf( "GCMIntentService.onRegistered(%s)", regId );
XWPrefs.setGCMDevID( context, regId );
}
@Override
- protected void onUnregistered( Context context, String regId )
+ protected void onUnregistered( Context context, String regId )
{
DbgUtils.logf( "GCMIntentService.onUnregistered(%s)", regId );
XWPrefs.clearGCMDevID( context );
@@ -51,44 +56,43 @@ public class GCMIntentService extends GCMBaseIntentService {
@Override
protected void onMessage( Context context, Intent intent )
{
- DbgUtils.logf( "GCMIntentService.onMessage(%s)", intent.toString() );
- boolean doRestartTimer = true; // keep a few days...
- String value = intent.getStringExtra( "msg" );
- if ( null != value ) {
- doRestartTimer = false; // expected key means new format
-
- String title = intent.getStringExtra( "title" );
- Utils.postNotification( context, null, title, value, 100000 );
- }
+ String value;
value = intent.getStringExtra( "getMoves" );
if ( null != value && Boolean.parseBoolean( value ) ) {
- doRestartTimer = true;
+ RelayReceiver.RestartTimer( context, true );
+ }
+ value = intent.getStringExtra( "checkUpdates" );
+ if ( null != value && Boolean.parseBoolean( value ) ) {
+ UpdateCheckReceiver.checkVersions( context, true );
}
- if ( doRestartTimer ) {
- RelayReceiver.RestartTimer( context, true );
+ value = intent.getStringExtra( "msg" );
+ if ( null != value ) {
+ String title = intent.getStringExtra( "title" );
+ if ( null != title ) {
+ int code = value.hashCode() ^ title.hashCode();
+ Utils.postNotification( context, null, title, value, code );
+ }
}
}
public static void init( Application app )
{
int sdkVersion = Integer.valueOf( android.os.Build.VERSION.SDK );
- if ( 8 <= sdkVersion ) {
+ if ( 8 <= sdkVersion && 0 < GCMConsts.SENDER_ID.length() ) {
try {
GCMRegistrar.checkDevice( app );
// GCMRegistrar.checkManifest( app );
- final String regId = GCMRegistrar.getRegistrationId( app );
+ String regId = XWPrefs.getGCMDevID( app );
if (regId.equals("")) {
GCMRegistrar.register( app, GCMConsts.SENDER_ID );
}
-
- String curID = XWPrefs.getGCMDevID( app );
- if ( ! curID.equals( regId ) ) {
- XWPrefs.setGCMDevID( app, regId );
- }
} catch ( UnsupportedOperationException uoe ) {
DbgUtils.logf( "Device can't do GCM." );
+ } catch ( Exception whatever ) {
+ // funky devices could do anything
+ DbgUtils.loge( whatever );
}
}
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameConfig.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameConfig.java
index 5fcf33251..c35139e61 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameConfig.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameConfig.java
@@ -92,7 +92,7 @@ public class GameConfig extends XWActivity
private boolean m_forResult;
private CurGameInfo m_gi;
private CurGameInfo m_giOrig;
- private GameUtils.GameLock m_gameLock;
+ private GameLock m_gameLock;
private int m_whichPlayer;
// private Spinner m_roleSpinner;
// private Spinner m_connectSpinner;
@@ -473,7 +473,7 @@ public class GameConfig extends XWActivity
// Lock in case we're going to config. We *could* re-get the
// lock once the user decides to make changes. PENDING.
- m_gameLock = new GameUtils.GameLock( m_rowid, true ).lock();
+ m_gameLock = new GameLock( m_rowid, true ).lock();
int gamePtr = GameUtils.loadMakeGame( this, m_giOrig, m_gameLock );
if ( 0 == gamePtr ) {
showDictGoneFinish();
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListAdapter.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListAdapter.java
index c234cb09a..4a6290d7e 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListAdapter.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListAdapter.java
@@ -1,7 +1,7 @@
/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
/*
- * Copyright 2009-2010 by Eric House (xwords@eehouse.org). All
- * rights reserved.
+ * Copyright 2009-2012 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
@@ -20,316 +20,113 @@
package org.eehouse.android.xw4;
import android.content.Context;
-import android.database.DataSetObserver;
-import android.os.AsyncTask;
-import android.os.Build;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
import android.widget.ListAdapter;
-import android.widget.TextView;
-import java.io.FileInputStream;
-import java.text.DateFormat;
-import java.util.Date;
-import java.util.HashMap; // class is not synchronized
-import java.util.Random;
+import android.widget.ListView;
import junit.framework.Assert;
-
import org.eehouse.android.xw4.jni.*;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
public class GameListAdapter extends XWListAdapter {
private Context m_context;
+ private ListView m_list;
private LayoutInflater m_factory;
private int m_fieldID;
private Handler m_handler;
- private static final boolean s_isFire;
- private static Random s_random;
- static {
- s_isFire = Build.MANUFACTURER.equals( "Amazon" );
- if ( s_isFire ) {
- s_random = new Random();
- }
- }
-
- private class ViewInfo implements View.OnClickListener {
- private View m_view;
- private View m_hideable;
- private ExpiringTextView m_name;
- private boolean m_expanded, m_haveTurn, m_haveTurnLocal;
- private long m_rowid;
- private long m_lastMoveTime;
- private ImageButton m_expandButton;
-
- public ViewInfo( View view, long rowid )
- {
- m_view = view;
- m_rowid = rowid;
- m_lastMoveTime = 0;
- }
-
- public ViewInfo( View view, long rowid, boolean expanded,
- long lastMoveTime, boolean haveTurn,
- boolean haveTurnLocal ) {
- this( view, rowid );
- m_expanded = expanded;
- m_lastMoveTime = lastMoveTime;
- m_haveTurn = haveTurn;
- m_haveTurnLocal = haveTurnLocal;
- m_hideable = (LinearLayout)view.findViewById( R.id.hideable );
- m_name = (ExpiringTextView)m_view.findViewById( R.id.game_name );
- m_expandButton = (ImageButton)view.findViewById( R.id.expander );
- m_expandButton.setOnClickListener( this );
- showHide();
- }
-
- private void showHide()
- {
- m_expandButton.setImageResource( m_expanded ?
- R.drawable.expander_ic_maximized :
- R.drawable.expander_ic_minimized);
- m_hideable.setVisibility( m_expanded? View.VISIBLE : View.GONE );
-
- m_name.setBackgroundColor( android.R.color.transparent );
- m_name.setPct( m_handler, m_haveTurn && !m_expanded,
- m_haveTurnLocal, m_lastMoveTime );
- }
-
- public void onClick( View view ) {
- m_expanded = !m_expanded;
- DBUtils.setExpanded( m_rowid, m_expanded );
- showHide();
- }
- }
-
- private HashMap m_viewsCache;
- private DateFormat m_df;
private LoadItemCB m_cb;
-
public interface LoadItemCB {
- public void itemLoaded( long rowid );
- public void itemClicked( long rowid );
+ public void itemClicked( long rowid, GameSummary summary );
}
- private class LoadItemTask extends AsyncTask {
- private long m_rowid;
- private Context m_context;
- // private int m_id;
- public LoadItemTask( Context context, long rowid/*, int id*/ )
- {
- m_context = context;
- m_rowid = rowid;
- // m_id = id;
- }
-
- @Override
- protected Void doInBackground( Void... unused )
- {
- // Without this, on the Fire only the last item in the
- // list it tappable. Likely my fault, but this seems to
- // work around it.
- if ( s_isFire ) {
- try {
- int sleepTime = 500 + (s_random.nextInt() % 500);
- Thread.sleep( sleepTime );
- } catch ( Exception e ) {
- }
- }
- View layout = m_factory.inflate( R.layout.game_list_item, null );
- boolean hideTitle = false;//CommonPrefs.getHideTitleBar(m_context);
- GameSummary summary = DBUtils.getSummary( m_context, m_rowid, 1500 );
- if ( null == summary ) {
- m_rowid = -1;
- } else {
- String state = summary.summarizeState();
-
- TextView view = (TextView)layout.findViewById( R.id.game_name );
- if ( hideTitle ) {
- view.setVisibility( View.GONE );
- } else {
- String value = null;
- switch ( m_fieldID ) {
- case R.string.game_summary_field_empty:
- break;
- case R.string.game_summary_field_language:
- value =
- DictLangCache.getLangName( m_context,
- summary.dictLang );
- break;
- case R.string.game_summary_field_opponents:
- value = summary.playerNames();
- break;
- case R.string.game_summary_field_state:
- value = state;
- break;
- }
-
- String name = GameUtils.getName( m_context, m_rowid );
-
- if ( null != value ) {
- value = m_context.getString( R.string.str_game_namef,
- name, value );
- } else {
- value = name;
- }
-
- view.setText( value );
- }
-
- layout.setOnClickListener( new View.OnClickListener() {
- @Override
- public void onClick( View v ) {
- m_cb.itemClicked( m_rowid );
- }
- } );
-
- LinearLayout list =
- (LinearLayout)layout.findViewById( R.id.player_list );
- boolean haveATurn = false;
- boolean haveALocalTurn = false;
- boolean[] isLocal = new boolean[1];
- for ( int ii = 0; ii < summary.nPlayers; ++ii ) {
- ExpiringLinearLayout tmp = (ExpiringLinearLayout)
- m_factory.inflate( R.layout.player_list_elem, null );
- view = (TextView)tmp.findViewById( R.id.item_name );
- view.setText( summary.summarizePlayer( ii ) );
- view = (TextView)tmp.findViewById( R.id.item_score );
- view.setText( String.format( " %d", summary.scores[ii] ) );
- boolean thisHasTurn = summary.isNextToPlay( ii, isLocal );
- if ( thisHasTurn ) {
- haveATurn = true;
- if ( isLocal[0] ) {
- haveALocalTurn = true;
- }
- }
- tmp.setPct( m_handler, thisHasTurn, isLocal[0],
- summary.lastMoveTime );
- list.addView( tmp, ii );
- }
-
- view = (TextView)layout.findViewById( R.id.state );
- view.setText( state );
- view = (TextView)layout.findViewById( R.id.modtime );
- long lastMoveTime = summary.lastMoveTime;
- lastMoveTime *= 1000;
- view.setText( m_df.format( new Date( lastMoveTime ) ) );
-
- int iconID;
- ImageView marker =
- (ImageView)layout.findViewById( R.id.msg_marker );
- CommsConnType conType = summary.conType;
- if ( CommsConnType.COMMS_CONN_RELAY == conType ) {
- iconID = R.drawable.relaygame;
- } else if ( CommsConnType.COMMS_CONN_BT == conType ) {
- iconID = android.R.drawable.stat_sys_data_bluetooth;
- } else if ( CommsConnType.COMMS_CONN_SMS == conType ) {
- iconID = android.R.drawable.sym_action_chat;
- } else {
- iconID = R.drawable.sologame;
- }
- marker.setImageResource( iconID );
-
- view = (TextView)layout.findViewById( R.id.role );
- String roleSummary = summary.summarizeRole();
- if ( null != roleSummary ) {
- view.setText( roleSummary );
- } else {
- view.setVisibility( View.GONE );
- }
-
- boolean expanded = DBUtils.getExpanded( m_context, m_rowid );
- ViewInfo vi = new ViewInfo( layout, m_rowid, expanded,
- summary.lastMoveTime, haveATurn,
- haveALocalTurn );
-
- synchronized( m_viewsCache ) {
- m_viewsCache.put( m_rowid, vi );
- }
- }
- return null;
- } // doInBackground
-
- @Override
- protected void onPostExecute( Void unused )
- {
- // DbgUtils.logf( "onPostExecute(rowid=%d)", m_rowid );
- if ( -1 != m_rowid ) {
- m_cb.itemLoaded( m_rowid );
- }
- }
- } // class LoadItemTask
-
- public GameListAdapter( Context context, Handler handler, LoadItemCB cb ) {
+ public GameListAdapter( Context context, ListView list,
+ Handler handler, LoadItemCB cb, String fieldName ) {
super( DBUtils.gamesList(context).length );
m_context = context;
+ m_list = list;
m_handler = handler;
m_cb = cb;
m_factory = LayoutInflater.from( context );
- m_df = DateFormat.getDateTimeInstance( DateFormat.SHORT,
- DateFormat.SHORT );
- m_viewsCache = new HashMap();
+ m_fieldID = fieldToID( fieldName );
}
-
+
+ @Override
public int getCount() {
return DBUtils.gamesList(m_context).length;
}
-
- public Object getItem( int position )
+
+ // Views. A view depends on a summary, which takes time to load.
+ // When one needs loading it's done via an async task.
+ public View getView( int position, View convertView, ViewGroup parent )
{
- final long rowid = DBUtils.gamesList(m_context)[position];
- View layout;
- boolean haveLayout = false;
- synchronized( m_viewsCache ) {
- ViewInfo vi = m_viewsCache.get( rowid );
- haveLayout = null != vi;
- if ( haveLayout ) {
- layout = vi.m_view;
- } else {
- layout = m_factory.inflate( R.layout.game_list_tmp, null );
- vi = new ViewInfo( layout, rowid );
- m_viewsCache.put( rowid, vi );
- }
- }
-
- if ( !haveLayout ) {
- new LoadItemTask( m_context, rowid/*, ++m_taskCounter*/ ).execute();
- }
-
- // this doesn't work. Rather, it breaks highlighting because
- // the background, if we don't set it, is a more complicated
- // object like @android:drawable/list_selector_background. I
- // tried calling getBackground(), expecting to get a Drawable
- // I could then clone and modify, but null comes back. So
- // layout must be inheriting its background from elsewhere or
- // it gets set later, during layout.
- // if ( (position%2) == 0 ) {
- // layout.setBackgroundColor( 0xFF3F3F3F );
- // }
-
- return layout;
- } // getItem
-
- public View getView( int position, View convertView, ViewGroup parent ) {
- return (View)getItem( position );
+ GameListItem result = (GameListItem)
+ m_factory.inflate( R.layout.game_list_item, null );
+ result.init( m_handler, DBUtils.gamesList(m_context)[position],
+ m_fieldID, m_cb );
+ return result;
}
public void inval( long rowid )
{
- synchronized( m_viewsCache ) {
- m_viewsCache.remove( rowid );
+ GameListItem child = getItemFor( rowid );
+ if ( null != child && child.getRowID() == rowid ) {
+ child.forceReload();
+ } else {
+ DbgUtils.logf( "no child for rowid %d", rowid );
+ GameListItem.inval( rowid );
+ m_list.invalidate();
}
}
- public void setField( String field )
+ public void invalName( long rowid )
+ {
+ GameListItem item = getItemFor( rowid );
+ if ( null != item ) {
+ item.invalName();
+ }
+ }
+
+ public boolean setField( String fieldName )
+ {
+ boolean changed = false;
+ int newID = fieldToID( fieldName );
+ if ( -1 == newID ) {
+ if ( XWApp.DEBUG ) {
+ DbgUtils.logf( "GameListAdapter.setField(): unable to match"
+ + " fieldName %s", fieldName );
+ }
+ } else if ( m_fieldID != newID ) {
+ if ( XWApp.DEBUG ) {
+ DbgUtils.logf( "setField: clearing views cache for change"
+ + " from %d to %d", m_fieldID, newID );
+ }
+ m_fieldID = newID;
+ // return true so caller will do onContentChanged.
+ // There's no other way to signal GameListItem instances
+ // since we don't maintain a list of them.
+ changed = true;
+ }
+ return changed;
+ }
+
+ private GameListItem getItemFor( long rowid )
+ {
+ GameListItem result = null;
+ int position = positionFor( rowid );
+ if ( 0 <= position ) {
+ result = (GameListItem)m_list.getChildAt( position );
+ }
+ return result;
+ }
+
+ private int fieldToID( String fieldName )
{
int[] ids = {
R.string.game_summary_field_empty
@@ -339,15 +136,24 @@ public class GameListAdapter extends XWListAdapter {
};
int result = -1;
for ( int id : ids ) {
- if ( m_context.getString( id ).equals( field ) ) {
+ if ( m_context.getString( id ).equals( fieldName ) ) {
result = id;
break;
}
}
- if ( m_fieldID != result ) {
- m_viewsCache.clear();
- m_fieldID = result;
- }
+ return result;
}
+ private int positionFor( long rowid )
+ {
+ int position = -1;
+ long[] rowids = DBUtils.gamesList( m_context );
+ for ( int ii = 0; ii < rowids.length; ++ii ) {
+ if ( rowids[ii] == rowid ) {
+ position = ii;
+ break;
+ }
+ }
+ return position;
+ }
}
\ No newline at end of file
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListItem.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListItem.java
new file mode 100644
index 000000000..9cd376d18
--- /dev/null
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListItem.java
@@ -0,0 +1,322 @@
+/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
+/*
+ * Copyright 2009-2012 by Eric House (xwords@eehouse.org). All rights
+ * reserved.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+package org.eehouse.android.xw4;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.os.AsyncTask;
+import android.os.Handler;
+// import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.HashSet;
+// import java.util.Iterator;
+
+import org.eehouse.android.xw4.jni.GameSummary;
+import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
+
+public class GameListItem extends LinearLayout
+ implements View.OnClickListener {
+
+ private static HashSet s_invalRows = new HashSet();
+
+ private Context m_context;
+ private boolean m_loaded;
+ private long m_rowid;
+ private View m_hideable;
+ private ExpiringTextView m_name;
+ private boolean m_expanded, m_haveTurn, m_haveTurnLocal;
+ private long m_lastMoveTime;
+ private ImageButton m_expandButton;
+ private Handler m_handler;
+ private GameSummary m_summary;
+ private GameListAdapter.LoadItemCB m_cb;
+ private int m_fieldID;
+ private int m_loadingCount;
+
+ public GameListItem( Context cx, AttributeSet as )
+ {
+ super( cx, as );
+ m_context = cx;
+ m_loaded = false;
+ m_rowid = DBUtils.ROWID_NOTFOUND;
+ m_lastMoveTime = 0;
+ m_loadingCount = 0;
+ }
+
+ public void init( Handler handler, long rowid, int fieldID,
+ GameListAdapter.LoadItemCB cb )
+ {
+ m_handler = handler;
+ m_rowid = rowid;
+ m_fieldID = fieldID;
+ m_cb = cb;
+
+ forceReload();
+ }
+
+ public void forceReload()
+ {
+ // DbgUtils.logf( "GameListItem.forceReload: rowid=%d", m_rowid );
+ m_summary = null;
+ setLoaded( false );
+ // Apparently it's impossible to reliably cancel an existing
+ // AsyncTask, so let it complete, but drop the results as soon
+ // as we're back on the UI thread.
+ ++m_loadingCount;
+ new LoadItemTask().execute();
+ }
+
+ public void invalName()
+ {
+ setName();
+ }
+
+ @Override
+ protected void onDraw( Canvas canvas )
+ {
+ super.onDraw( canvas );
+ if ( DBUtils.ROWID_NOTFOUND != m_rowid ) {
+ synchronized( s_invalRows ) {
+ if ( s_invalRows.contains( m_rowid ) ) {
+ forceReload();
+ }
+ }
+ }
+ }
+
+ private void update( boolean expanded, long lastMoveTime, boolean haveTurn,
+ boolean haveTurnLocal )
+ {
+ m_expanded = expanded;
+ m_lastMoveTime = lastMoveTime;
+ m_haveTurn = haveTurn;
+ m_haveTurnLocal = haveTurnLocal;
+ m_hideable = (LinearLayout)findViewById( R.id.hideable );
+ m_name = (ExpiringTextView)findViewById( R.id.game_name );
+ m_expandButton = (ImageButton)findViewById( R.id.expander );
+ m_expandButton.setOnClickListener( this );
+ showHide();
+ }
+
+ public long getRowID()
+ {
+ return m_rowid;
+ }
+
+ // View.OnClickListener interface
+ public void onClick( View view ) {
+ m_expanded = !m_expanded;
+ DBUtils.setExpanded( m_rowid, m_expanded );
+ showHide();
+ }
+
+ private void setLoaded( boolean loaded )
+ {
+ if ( loaded != m_loaded ) {
+ m_loaded = loaded;
+ // This should be enough to invalidate
+ findViewById( R.id.view_unloaded )
+ .setVisibility( loaded ? View.GONE : View.VISIBLE );
+ findViewById( R.id.view_loaded )
+ .setVisibility( loaded ? View.VISIBLE : View.GONE );
+ }
+ }
+
+ private void showHide()
+ {
+ m_expandButton.setImageResource( m_expanded ?
+ R.drawable.expander_ic_maximized :
+ R.drawable.expander_ic_minimized);
+ m_hideable.setVisibility( m_expanded? View.VISIBLE : View.GONE );
+
+ m_name.setBackgroundColor( android.R.color.transparent );
+ m_name.setPct( m_handler, m_haveTurn && !m_expanded,
+ m_haveTurnLocal, m_lastMoveTime );
+ }
+
+ private String setName()
+ {
+ String state = null; // hack to avoid calling summarizeState twice
+ if ( null != m_summary ) {
+ state = m_summary.summarizeState();
+ TextView view = (TextView)findViewById( R.id.game_name );
+ String value = null;
+ switch ( m_fieldID ) {
+ case R.string.game_summary_field_empty:
+ break;
+ case R.string.game_summary_field_language:
+ value =
+ DictLangCache.getLangName( m_context,
+ m_summary.dictLang );
+ break;
+ case R.string.game_summary_field_opponents:
+ value = m_summary.playerNames();
+ break;
+ case R.string.game_summary_field_state:
+ value = state;
+ break;
+ }
+
+ if ( null != value ) {
+ String name = GameUtils.getName( m_context, m_rowid );
+ value = m_context.getString( R.string.str_game_namef,
+ name, value );
+ } else {
+ value = GameUtils.getName( m_context, m_rowid );
+ }
+
+ view.setText( value );
+ }
+ return state;
+ }
+
+ private void setData( final GameSummary summary )
+ {
+ if ( null != summary ) {
+ TextView view;
+ String state = setName();
+
+ setOnClickListener( new View.OnClickListener() {
+ @Override
+ public void onClick( View v ) {
+ m_cb.itemClicked( m_rowid, summary );
+ }
+ } );
+
+ LinearLayout list =
+ (LinearLayout)findViewById( R.id.player_list );
+ list.removeAllViews();
+ boolean haveATurn = false;
+ boolean haveALocalTurn = false;
+ boolean[] isLocal = new boolean[1];
+ for ( int ii = 0; ii < summary.nPlayers; ++ii ) {
+ ExpiringLinearLayout tmp = (ExpiringLinearLayout)
+ Utils.inflate( m_context, R.layout.player_list_elem );
+ view = (TextView)tmp.findViewById( R.id.item_name );
+ view.setText( summary.summarizePlayer( ii ) );
+ view = (TextView)tmp.findViewById( R.id.item_score );
+ view.setText( String.format( " %d", summary.scores[ii] ) );
+ boolean thisHasTurn = summary.isNextToPlay( ii, isLocal );
+ if ( thisHasTurn ) {
+ haveATurn = true;
+ if ( isLocal[0] ) {
+ haveALocalTurn = true;
+ }
+ }
+ tmp.setPct( m_handler, thisHasTurn, isLocal[0],
+ summary.lastMoveTime );
+ list.addView( tmp, ii );
+ }
+
+ view = (TextView)findViewById( R.id.state );
+ view.setText( state );
+ view = (TextView)findViewById( R.id.modtime );
+ long lastMoveTime = summary.lastMoveTime;
+ lastMoveTime *= 1000;
+
+ DateFormat df = DateFormat.getDateTimeInstance( DateFormat.SHORT,
+ DateFormat.SHORT );
+ view.setText( df.format( new Date( lastMoveTime ) ) );
+
+ int iconID;
+ ImageView marker =
+ (ImageView)findViewById( R.id.msg_marker );
+ CommsConnType conType = summary.conType;
+ if ( CommsConnType.COMMS_CONN_RELAY == conType ) {
+ iconID = R.drawable.relaygame;
+ } else if ( CommsConnType.COMMS_CONN_BT == conType ) {
+ iconID = android.R.drawable.stat_sys_data_bluetooth;
+ } else if ( CommsConnType.COMMS_CONN_SMS == conType ) {
+ iconID = android.R.drawable.sym_action_chat;
+ } else {
+ iconID = R.drawable.sologame;
+ }
+ marker.setImageResource( iconID );
+
+ view = (TextView)findViewById( R.id.role );
+ String roleSummary = summary.summarizeRole();
+ if ( null != roleSummary ) {
+ view.setText( roleSummary );
+ } else {
+ view.setVisibility( View.GONE );
+ }
+
+ boolean expanded = DBUtils.getExpanded( m_context, m_rowid );
+
+ update( expanded, summary.lastMoveTime, haveATurn,
+ haveALocalTurn );
+ }
+ }
+
+ private class LoadItemTask extends AsyncTask {
+ @Override
+ protected GameSummary doInBackground( Void... unused )
+ {
+ return DBUtils.getSummary( m_context, m_rowid, 150 );
+ } // doInBackground
+
+ @Override
+ protected void onPostExecute( GameSummary summary )
+ {
+ if ( 0 == --m_loadingCount ) {
+ m_summary = summary;
+ setData( summary );
+ setLoaded( null != m_summary );
+ synchronized( s_invalRows ) {
+ s_invalRows.remove( m_rowid );
+ }
+ }
+ // DbgUtils.logf( "LoadItemTask for row %d finished; "
+ // + "inval rows now %s",
+ // m_rowid, invalRowsToString() );
+ }
+ } // class LoadItemTask
+
+ public static void inval( long rowid )
+ {
+ synchronized( s_invalRows ) {
+ s_invalRows.add( rowid );
+ }
+ // DbgUtils.logf( "GameListItem.inval(rowid=%d); inval rows now %s",
+ // rowid, invalRowsToString() );
+ }
+
+ // private static String invalRowsToString()
+ // {
+ // String[] strs;
+ // synchronized( s_invalRows ) {
+ // strs = new String[s_invalRows.size()];
+ // Iterator iter = s_invalRows.iterator();
+ // for ( int ii = 0; iter.hasNext(); ++ii ) {
+ // strs[ii] = String.format("%d", iter.next() );
+ // }
+ // }
+ // return TextUtils.join(",", strs );
+ // }
+
+}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameLock.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameLock.java
new file mode 100644
index 000000000..12c080c23
--- /dev/null
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameLock.java
@@ -0,0 +1,165 @@
+/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
+/*
+ * Copyright 2009-2010 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 java.util.HashMap;
+
+import junit.framework.Assert;
+
+// Implements read-locks and write-locks per game. A read lock is
+// obtainable when other read locks are granted but not when a
+// write lock is. Write-locks are exclusive.
+public class GameLock {
+ private long m_rowid;
+ private boolean m_isForWrite;
+ private int m_lockCount;
+ StackTraceElement[] m_lockTrace;
+
+ private static HashMap
+ s_locks = new HashMap();
+
+ public GameLock( long rowid, boolean isForWrite )
+ {
+ m_rowid = rowid;
+ m_isForWrite = isForWrite;
+ m_lockCount = 0;
+ if ( XWApp.DEBUG_LOCKS ) {
+ DbgUtils.logf( "GameLock.GameLock(rowid:%d,isForWrite:%b)=>"
+ + "this: %H", rowid, isForWrite, this );
+ DbgUtils.printStack();
+ }
+ }
+
+ // This could be written to allow multiple read locks. Let's
+ // see if not doing that causes problems.
+ public boolean tryLock()
+ {
+ boolean gotIt = false;
+ synchronized( s_locks ) {
+ GameLock owner = s_locks.get( m_rowid );
+ if ( null == owner ) { // unowned
+ Assert.assertTrue( 0 == m_lockCount );
+ s_locks.put( m_rowid, this );
+ ++m_lockCount;
+ gotIt = true;
+
+ if ( XWApp.DEBUG_LOCKS ) {
+ StackTraceElement[] trace = Thread.currentThread().
+ getStackTrace();
+ m_lockTrace = new StackTraceElement[trace.length];
+ System.arraycopy( trace, 0, m_lockTrace, 0, trace.length );
+ }
+ } else if ( this == owner && ! m_isForWrite ) {
+ Assert.assertTrue( 0 == m_lockCount );
+ ++m_lockCount;
+ gotIt = true;
+ }
+ }
+ return gotIt;
+ }
+
+ // Wait forever (but may assert if too long)
+ public GameLock lock()
+ {
+ return this.lock( 0 );
+ }
+
+ // Version that's allowed to return null -- if maxMillis > 0
+ public GameLock lock( long maxMillis )
+ {
+ GameLock result = null;
+ final long assertTime = 2000;
+ Assert.assertTrue( maxMillis < assertTime );
+ long sleptTime = 0;
+
+ if ( XWApp.DEBUG_LOCKS ) {
+ DbgUtils.logf( "lock %H (rowid:%d, maxMillis=%d)", this, m_rowid, maxMillis );
+ }
+
+ for ( ; ; ) {
+ if ( tryLock() ) {
+ result = this;
+ break;
+ }
+ if ( XWApp.DEBUG_LOCKS ) {
+ DbgUtils.logf( "GameLock.lock() %H failed; sleeping", this );
+ DbgUtils.printStack();
+ }
+ try {
+ Thread.sleep( 25 ); // milliseconds
+ sleptTime += 25;
+ } catch( InterruptedException ie ) {
+ DbgUtils.loge( ie );
+ break;
+ }
+
+ if ( XWApp.DEBUG_LOCKS ) {
+ DbgUtils.logf( "GameLock.lock() %H awake; "
+ + "sleptTime now %d millis", this, sleptTime );
+ }
+
+ if ( 0 < maxMillis && sleptTime >= maxMillis ) {
+ break;
+ } else if ( sleptTime >= assertTime ) {
+ if ( XWApp.DEBUG_LOCKS ) {
+ DbgUtils.logf( "lock %H overlocked. lock holding stack:",
+ this );
+ DbgUtils.printStack( m_lockTrace );
+ DbgUtils.logf( "lock %H seeking stack:", this );
+ DbgUtils.printStack();
+ }
+ Assert.fail();
+ }
+ }
+ // DbgUtils.logf( "GameLock.lock(%s) done", m_path );
+ return result;
+ }
+
+ public void unlock()
+ {
+ // DbgUtils.logf( "GameLock.unlock(%s)", m_path );
+ synchronized( s_locks ) {
+ Assert.assertTrue( this == s_locks.get(m_rowid) );
+ if ( 1 == m_lockCount ) {
+ s_locks.remove( m_rowid );
+ } else {
+ Assert.assertTrue( !m_isForWrite );
+ }
+ --m_lockCount;
+
+ if ( XWApp.DEBUG_LOCKS ) {
+ DbgUtils.logf( "GameLock.unlock: this: %H (rowid:%d) unlocked",
+ this, m_rowid );
+ }
+ }
+ }
+
+ public long getRowid()
+ {
+ return m_rowid;
+ }
+
+ // used only for asserts
+ public boolean canWrite()
+ {
+ return m_isForWrite && 1 == m_lockCount;
+ }
+}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java
index f89c3a99b..37b765f6a 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java
@@ -24,19 +24,16 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
-import android.os.Environment;
+import android.text.Html;
+import android.text.TextUtils;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileOutputStream;
-import java.io.InputStream;
-import java.nio.channels.FileChannel;
-import java.util.ArrayList;
import java.util.Arrays;
-import android.content.res.AssetManager;
-import java.util.concurrent.locks.Lock;
import java.util.HashMap;
import java.util.HashSet;
-import android.text.Html;
+import java.util.concurrent.locks.Lock;
+import org.json.JSONArray;
+import org.json.JSONObject;
import junit.framework.Assert;
@@ -49,134 +46,6 @@ public class GameUtils {
public static final String INTENT_KEY_ROWID = "rowid";
public static final String INTENT_FORRESULT_ROWID = "forresult";
- // Implements read-locks and write-locks per game. A read lock is
- // obtainable when other read locks are granted but not when a
- // write lock is. Write-locks are exclusive.
- public static class GameLock {
- private long m_rowid;
- private boolean m_isForWrite;
- private int m_lockCount;
- StackTraceElement[] m_lockTrace;
-
- private static HashMap
- s_locks = new HashMap();
-
- public GameLock( long rowid, boolean isForWrite )
- {
- m_rowid = rowid;
- m_isForWrite = isForWrite;
- m_lockCount = 0;
- if ( XWApp.DEBUG_LOCKS ) {
- DbgUtils.logf( "GameLock.GameLock(rowid:%d,isForWrite:%b)=>"
- + "this: %H", rowid, isForWrite, this );
- DbgUtils.printStack();
- }
- }
-
- // This could be written to allow multiple read locks. Let's
- // see if not doing that causes problems.
- public boolean tryLock()
- {
- boolean gotIt = false;
- synchronized( s_locks ) {
- GameLock owner = s_locks.get( m_rowid );
- if ( null == owner ) { // unowned
- Assert.assertTrue( 0 == m_lockCount );
- s_locks.put( m_rowid, this );
- ++m_lockCount;
- gotIt = true;
-
- if ( XWApp.DEBUG_LOCKS ) {
- StackTraceElement[] trace = Thread.currentThread().
- getStackTrace();
- m_lockTrace = new StackTraceElement[trace.length];
- System.arraycopy( trace, 0, m_lockTrace, 0, trace.length );
- }
- } else if ( this == owner && ! m_isForWrite ) {
- Assert.assertTrue( 0 == m_lockCount );
- ++m_lockCount;
- gotIt = true;
- }
- }
- return gotIt;
- }
-
- // Wait forever (but may assert if too long)
- public GameLock lock()
- {
- return this.lock( 0 );
- }
-
- // Version that's allowed to return null -- if maxMillis > 0
- public GameLock lock( long maxMillis )
- {
- GameLock result = null;
- final long assertTime = 2000;
- Assert.assertTrue( maxMillis < assertTime );
- long sleptTime = 0;
- // DbgUtils.logf( "GameLock.lock(%s)", m_path );
- // Utils.printStack();
- for ( ; ; ) {
- if ( tryLock() ) {
- result = this;
- break;
- }
- if ( XWApp.DEBUG_LOCKS ) {
- DbgUtils.logf( "GameLock.lock() %H failed; sleeping", this );
- DbgUtils.printStack();
- }
- try {
- Thread.sleep( 25 ); // milliseconds
- sleptTime += 25;
- } catch( InterruptedException ie ) {
- DbgUtils.loge( ie );
- break;
- }
-
- if ( 0 < maxMillis && sleptTime >= maxMillis ) {
- break;
- } else if ( sleptTime >= assertTime ) {
- if ( XWApp.DEBUG_LOCKS ) {
- DbgUtils.logf( "lock %H overlocked. lock holding stack:",
- this );
- DbgUtils.printStack( m_lockTrace );
- DbgUtils.logf( "lock %H seeking stack:", this );
- DbgUtils.printStack();
- }
- Assert.fail();
- }
- }
- // DbgUtils.logf( "GameLock.lock(%s) done", m_path );
- return result;
- }
-
- public void unlock()
- {
- // DbgUtils.logf( "GameLock.unlock(%s)", m_path );
- synchronized( s_locks ) {
- Assert.assertTrue( this == s_locks.get(m_rowid) );
- if ( 1 == m_lockCount ) {
- s_locks.remove( m_rowid );
- } else {
- Assert.assertTrue( !m_isForWrite );
- }
- --m_lockCount;
- }
- // DbgUtils.logf( "GameLock.unlock(%s) done", m_path );
- }
-
- public long getRowid()
- {
- return m_rowid;
- }
-
- // used only for asserts
- public boolean canWrite()
- {
- return m_isForWrite && 1 == m_lockCount;
- }
- }
-
private static Object s_syncObj = new Object();
public static byte[] savedGame( Context context, long rowid )
@@ -245,10 +114,16 @@ public class GameUtils {
public static void resetGame( Context context, long rowidIn )
{
- GameLock lock = new GameLock( rowidIn, true ).lock();
- tellDied( context, lock, true );
- resetGame( context, lock, lock, false );
- lock.unlock();
+ GameLock lock = new GameLock( rowidIn, true ).lock( 500 );
+ if ( null != lock ) {
+ tellDied( context, lock, true );
+ resetGame( context, lock, lock, false );
+ lock.unlock();
+
+ Utils.cancelNotification( context, (int)rowidIn );
+ } else {
+ DbgUtils.logf( "resetGame: unable to open rowid %d", rowidIn );
+ }
}
private static GameSummary summarizeAndClose( Context context,
@@ -301,12 +176,17 @@ public class GameUtils {
public static long dupeGame( Context context, long rowidIn )
{
- boolean juggle = CommonPrefs.getAutoJuggle( context );
- GameLock lockSrc = new GameLock( rowidIn, false ).lock();
- GameLock lockDest = resetGame( context, lockSrc, null, juggle );
- long rowid = lockDest.getRowid();
- lockDest.unlock();
- lockSrc.unlock();
+ long rowid = DBUtils.ROWID_NOTFOUND;
+ GameLock lockSrc = new GameLock( rowidIn, false ).lock( 300 );
+ if ( null != lockSrc ) {
+ boolean juggle = CommonPrefs.getAutoJuggle( context );
+ GameLock lockDest = resetGame( context, lockSrc, null, juggle );
+ rowid = lockDest.getRowid();
+ lockDest.unlock();
+ lockSrc.unlock();
+ } else {
+ DbgUtils.logf( "dupeGame: unable to open rowid %d", rowidIn );
+ }
return rowid;
}
@@ -318,6 +198,7 @@ public class GameUtils {
GameLock lock = new GameLock( rowid, true );
if ( lock.tryLock() ) {
tellDied( context, lock, informNow );
+ Utils.cancelNotification( context, (int)rowid );
DBUtils.deleteGame( context, lock );
lock.unlock();
} else {
@@ -351,7 +232,8 @@ public class GameUtils {
String[] dictNames = gi.dictNames();
DictUtils.DictPairs pairs = DictUtils.openDicts( context, dictNames );
if ( pairs.anyMissing( dictNames ) ) {
- DbgUtils.logf( "loadMakeGame() failing: dict unavailable" );
+ DbgUtils.logf( "loadMakeGame() failing: dicts %s unavailable",
+ TextUtils.join( ",", dictNames ) );
} else {
gamePtr = XwJNI.initJNI();
@@ -415,7 +297,7 @@ public class GameUtils {
}
private static long makeNewMultiGame( Context context, CommsAddrRec addr,
- int[] lang, String dict,
+ int[] lang, String[] dict,
int nPlayersT, int nPlayersH,
String inviteID, int gameID,
boolean isHost )
@@ -423,8 +305,9 @@ public class GameUtils {
long rowid = -1;
CurGameInfo gi = new CurGameInfo( context, true );
- gi.setLang( lang[0], dict );
+ gi.setLang( lang[0], dict[0] );
lang[0] = gi.dictLang;
+ dict[0] = gi.dictName;
gi.setNPlayers( nPlayersT, nPlayersH );
gi.juggle();
if ( 0 != gameID ) {
@@ -449,7 +332,8 @@ public class GameUtils {
public static long makeNewNetGame( Context context, String room,
String inviteID, int[] lang,
- int nPlayersT, int nPlayersH )
+ String[] dict, int nPlayersT,
+ int nPlayersH )
{
long rowid = -1;
String relayName = XWPrefs.getDefaultRelayHost( context );
@@ -457,21 +341,24 @@ public class GameUtils {
CommsAddrRec addr = new CommsAddrRec( relayName, relayPort );
addr.ip_relay_invite = room;
- return makeNewMultiGame( context, addr, lang, null, nPlayersT,
+ return makeNewMultiGame( context, addr, lang, dict, nPlayersT,
nPlayersH, inviteID, 0, false );
}
public static long makeNewNetGame( Context context, String room,
- String inviteID, int lang, int nPlayers )
+ String inviteID, int lang, String dict,
+ int nPlayers )
{
int[] langarr = { lang };
- return makeNewNetGame( context, room, inviteID, langarr, nPlayers, 1 );
+ String[] dictArr = { dict };
+ return makeNewNetGame( context, room, inviteID, langarr, dictArr,
+ nPlayers, 1 );
}
public static long makeNewNetGame( Context context, NetLaunchInfo info )
{
return makeNewNetGame( context, info.room, info.inviteID, info.lang,
- info.nPlayers );
+ info.dict, info.nPlayersT );
}
public static long makeNewBTGame( Context context, int gameID,
@@ -495,40 +382,26 @@ public class GameUtils {
{
long rowid = -1;
int[] langa = { lang };
+ String[] dicta = { dict };
boolean isHost = null == addr;
if ( isHost ) {
addr = new CommsAddrRec(CommsAddrRec.CommsConnType.COMMS_CONN_SMS);
}
- return makeNewMultiGame( context, addr, langa, dict, nPlayersT,
+ return makeNewMultiGame( context, addr, langa, dicta, nPlayersT,
nPlayersH, null, gameID, isHost );
}
- public static void launchBTInviter( Activity activity, int nMissing,
- int requestCode )
- {
- Intent intent = new Intent( activity, BTInviteActivity.class );
- intent.putExtra( BTInviteActivity.INTENT_KEY_NMISSING, nMissing );
- activity.startActivityForResult( intent, requestCode );
- }
-
- public static void launchSMSInviter( Activity activity, int nMissing,
- int requestCode )
- {
- Intent intent = new Intent( activity, SMSInviteActivity.class );
- intent.putExtra( SMSInviteActivity.INTENT_KEY_NMISSING, nMissing );
- activity.startActivityForResult( intent, requestCode );
- }
-
public static void launchInviteActivity( Context context,
boolean choseEmail,
String room, String inviteID,
- int lang, int nPlayers )
+ int lang, String dict,
+ int nPlayers )
{
if ( null == inviteID ) {
inviteID = makeRandomID();
}
Uri gameUri = NetLaunchInfo.makeLaunchUri( context, room, inviteID,
- lang, nPlayers );
+ lang, dict, nPlayers );
if ( null != gameUri ) {
int fmtId = choseEmail? R.string.invite_htmf : R.string.invite_txtf;
@@ -538,11 +411,28 @@ public class GameUtils {
Intent intent = new Intent();
if ( choseEmail ) {
intent.setAction( Intent.ACTION_SEND );
- intent.setType( "message/rfc822");
String subject =
Utils.format( context, R.string.invite_subjectf, room );
intent.putExtra( Intent.EXTRA_SUBJECT, subject );
intent.putExtra( Intent.EXTRA_TEXT, Html.fromHtml(message) );
+
+ File attach = null;
+ File tmpdir = XWApp.ATTACH_SUPPORTED ?
+ DictUtils.getDownloadDir( context ) : null;
+ if ( null != tmpdir ) { // no attachment
+ attach = makeJsonFor( tmpdir, room, inviteID, lang,
+ dict, nPlayers );
+ }
+
+ if ( null == attach ) { // no attachment
+ intent.setType( "message/rfc822");
+ } else {
+ String mime = context.getString( R.string.invite_mime );
+ intent.setType( mime );
+ Uri uri = Uri.fromFile( attach );
+ intent.putExtra( Intent.EXTRA_STREAM, uri );
+ }
+
choiceID = R.string.invite_chooser_email;
} else {
intent.setAction( Intent.ACTION_VIEW );
@@ -646,7 +536,6 @@ public class GameUtils {
boolean invited )
{
Intent intent = new Intent( activity, BoardActivity.class );
- intent.setAction( Intent.ACTION_EDIT );
intent.putExtra( INTENT_KEY_ROWID, rowid );
if ( invited ) {
intent.putExtra( INVITED, true );
@@ -696,39 +585,45 @@ public class GameUtils {
}
}
- private static boolean feedMessages( Context context, long rowid,
- byte[][] msgs, CommsAddrRec ret,
- MultiMsgSink sink )
+ public static boolean feedMessages( Context context, long rowid,
+ byte[][] msgs, CommsAddrRec ret,
+ MultiMsgSink sink )
{
boolean draw = false;
Assert.assertTrue( -1 != rowid );
- GameLock lock = new GameLock( rowid, true );
- if ( lock.tryLock() ) {
- CurGameInfo gi = new CurGameInfo( context );
- FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid );
- int gamePtr = loadMakeGame( context, gi, feedImpl, sink, lock );
-
- XwJNI.comms_resendAll( gamePtr, false );
+ if ( null != msgs ) {
+ // timed lock: If a game is opened by BoardActivity just
+ // as we're trying to deliver this message to it it'll
+ // have the lock and we'll never get it. Better to drop
+ // the message than fire the hung-lock assert. Messages
+ // belong in local pre-delivery storage anyway.
+ GameLock lock = new GameLock( rowid, true ).lock( 150 );
+ if ( null != lock ) {
+ CurGameInfo gi = new CurGameInfo( context );
+ FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid );
+ int gamePtr = loadMakeGame( context, gi, feedImpl, sink, lock );
+ if ( 0 != gamePtr ) {
+ XwJNI.comms_resendAll( gamePtr, false, false );
- if ( null != msgs ) {
- for ( byte[] msg : msgs ) {
- draw = XwJNI.game_receiveMessage( gamePtr, msg, ret )
- || draw;
+ for ( byte[] msg : msgs ) {
+ draw = XwJNI.game_receiveMessage( gamePtr, msg, ret )
+ || draw;
+ }
+ XwJNI.comms_ackAny( gamePtr );
+
+ // update gi to reflect changes due to messages
+ XwJNI.game_getGi( gamePtr, gi );
+ saveGame( context, gamePtr, gi, lock, false );
+ summarizeAndClose( context, lock, gamePtr, gi, feedImpl );
+
+ int flags = setFromFeedImpl( feedImpl );
+ if ( GameSummary.MSG_FLAGS_NONE != flags ) {
+ draw = true;
+ DBUtils.setMsgFlags( rowid, flags );
+ }
}
+ lock.unlock();
}
- XwJNI.comms_ackAny( gamePtr );
-
- // update gi to reflect changes due to messages
- XwJNI.game_getGi( gamePtr, gi );
- saveGame( context, gamePtr, gi, lock, false );
- summarizeAndClose( context, lock, gamePtr, gi, feedImpl );
-
- int flags = setFromFeedImpl( feedImpl );
- if ( GameSummary.MSG_FLAGS_NONE != flags ) {
- draw = true;
- DBUtils.setMsgFlags( rowid, flags );
- }
- lock.unlock();
}
return draw;
} // feedMessages
@@ -742,52 +637,45 @@ public class GameUtils {
return feedMessages( context, rowid, msgs, ret, sink );
}
- // Current assumption: this is the relay case where return address
- // can be null.
- public static boolean feedMessages( Context context, String relayID,
- byte[][] msgs, MultiMsgSink sink )
- {
- boolean draw = false;
- long[] rowids = DBUtils.getRowIDsFor( context, relayID );
- if ( null != rowids ) {
- for ( long rowid : rowids ) {
- draw = feedMessages( context, rowid, msgs, null, sink ) || draw;
- }
- }
- return draw;
- }
-
// This *must* involve a reset if the language is changing!!!
// Which isn't possible right now, so make sure the old and new
// dict have the same langauge code.
- public static void replaceDicts( Context context, long rowid,
- String oldDict, String newDict )
+ public static boolean replaceDicts( Context context, long rowid,
+ String oldDict, String newDict )
{
- GameLock lock = new GameLock( rowid, true ).lock();
- byte[] stream = savedGame( context, lock );
- CurGameInfo gi = new CurGameInfo( context );
- XwJNI.gi_from_stream( gi, stream );
+ GameLock lock = new GameLock( rowid, true ).lock(300);
+ boolean success = null != lock;
+ if ( success ) {
+ byte[] stream = savedGame( context, lock );
+ CurGameInfo gi = new CurGameInfo( context );
+ XwJNI.gi_from_stream( gi, stream );
- // first time required so dictNames() will work
- gi.replaceDicts( newDict );
+ // first time required so dictNames() will work
+ gi.replaceDicts( newDict );
- String[] dictNames = gi.dictNames();
- DictUtils.DictPairs pairs = DictUtils.openDicts( context, dictNames );
+ String[] dictNames = gi.dictNames();
+ DictUtils.DictPairs pairs = DictUtils.openDicts( context,
+ dictNames );
- int gamePtr = XwJNI.initJNI();
- XwJNI.game_makeFromStream( gamePtr, stream, gi, dictNames,
- pairs.m_bytes, pairs.m_paths,
- gi.langName(), JNIUtilsImpl.get(context),
- CommonPrefs.get( context ) );
- // second time required as game_makeFromStream can overwrite
- gi.replaceDicts( newDict );
+ int gamePtr = XwJNI.initJNI();
+ XwJNI.game_makeFromStream( gamePtr, stream, gi, dictNames,
+ pairs.m_bytes, pairs.m_paths,
+ gi.langName(),
+ JNIUtilsImpl.get(context),
+ CommonPrefs.get( context ) );
+ // second time required as game_makeFromStream can overwrite
+ gi.replaceDicts( newDict );
- saveGame( context, gamePtr, gi, lock, false );
+ saveGame( context, gamePtr, gi, lock, false );
- summarizeAndClose( context, lock, gamePtr, gi );
+ summarizeAndClose( context, lock, gamePtr, gi );
- lock.unlock();
- }
+ lock.unlock();
+ } else {
+ DbgUtils.logf( "replaceDicts: unable to open rowid %d", rowid );
+ }
+ return success;
+ } // replaceDicts
public static void applyChanges( Context context, CurGameInfo gi,
CommsAddrRec car, GameLock lock,
@@ -899,5 +787,31 @@ public class GameUtils {
}
}
+ private static File makeJsonFor( File dir, String room, String inviteID,
+ int lang, String dict, int nPlayers )
+ {
+ File result = null;
+ if ( XWApp.ATTACH_SUPPORTED ) {
+ JSONObject json = new JSONObject();
+ try {
+ json.put( MultiService.ROOM, room );
+ json.put( MultiService.INVITEID, inviteID );
+ json.put( MultiService.LANG, lang );
+ json.put( MultiService.DICT, dict );
+ json.put( MultiService.NPLAYERST, nPlayers );
+ byte[] data = json.toString().getBytes();
+
+ File file = new File( dir,
+ String.format("invite_%s", room ) );
+ FileOutputStream fos = new FileOutputStream( file );
+ fos.write( data, 0, data.length );
+ fos.close();
+ result = file;
+ } catch ( Exception ex ) {
+ DbgUtils.loge( ex );
+ }
+ }
+ return result;
+ }
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java
index 7484dd696..84bfba2ae 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java
@@ -20,30 +20,31 @@
package org.eehouse.android.xw4;
-import android.app.ListActivity;
-import android.app.Dialog;
import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ListActivity;
import android.app.PendingIntent;
import android.content.Context;
-import android.content.Intent;
import android.content.DialogInterface;
+import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
+import android.preference.PreferenceManager;
+import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextMenu;
import android.view.Menu;
+import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
-import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
+import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
-import android.widget.Button;
-import android.view.MenuInflater;
import java.io.File;
-import android.preference.PreferenceManager;
+import java.util.Date;
// import android.telephony.PhoneStateListener;
// import android.telephony.TelephonyManager;
import junit.framework.Assert;
@@ -51,20 +52,24 @@ import junit.framework.Assert;
import org.eehouse.android.xw4.jni.*;
public class GamesList extends XWListActivity
- implements DispatchNotify.HandleRelaysIface,
- DBUtils.DBChangeListener,
+ implements DBUtils.DBChangeListener,
GameListAdapter.LoadItemCB,
- NetUtils.DownloadFinishedListener {
+ DictImportActivity.DownloadFinishedListener {
private static final int WARN_NODICT = DlgDelegate.DIALOG_LAST + 1;
private static final int WARN_NODICT_SUBST = WARN_NODICT + 1;
- private static final int SHOW_SUBST = WARN_NODICT + 2;
- private static final int GET_NAME = WARN_NODICT + 3;
- private static final int RENAME_GAME = WARN_NODICT + 4;
+ private static final int WARN_NODICT_NEW = WARN_NODICT + 2;
+ private static final int SHOW_SUBST = WARN_NODICT + 3;
+ private static final int GET_NAME = WARN_NODICT + 4;
+ private static final int RENAME_GAME = WARN_NODICT + 5;
private static final String SAVE_ROWID = "SAVE_ROWID";
private static final String SAVE_DICTNAMES = "SAVE_DICTNAMES";
+ private static final String RELAYIDS_EXTRA = "relayids";
+ private static final String GAMEID_EXTRA = "gameid";
+ private static final String REMATCH_ROWID_EXTRA = "rowid";
+
private static final int NEW_NET_GAME_ACTION = 1;
private static final int RESET_GAME_ACTION = 2;
private static final int DELETE_GAME_ACTION = 3;
@@ -80,14 +85,12 @@ public class GamesList extends XWListActivity
private GameListAdapter m_adapter;
private String m_missingDict;
- private String[] m_missingDictNames;
- private long m_missingDictRowId;
+ private String m_missingDictName;
+ private long m_missingDictRowId = DBUtils.ROWID_NOTFOUND;
private String[] m_sameLangDicts;
private int m_missingDictLang;
private long m_rowid;
- private String m_nameField;
private NetLaunchInfo m_netLaunchInfo;
- // private String m_smsPhone;
@Override
protected Dialog onCreateDialog( int id )
@@ -100,14 +103,21 @@ public class GamesList extends XWListActivity
AlertDialog.Builder ab;
switch ( id ) {
case WARN_NODICT:
+ case WARN_NODICT_NEW:
case WARN_NODICT_SUBST:
lstnr = new DialogInterface.OnClickListener() {
public void onClick( DialogInterface dlg, int item ) {
- // just do one
- NetUtils.downloadDictInBack( GamesList.this,
+ // no name, so user must pick
+ if ( null == m_missingDictName ) {
+ DictsActivity.launchAndDownload( GamesList.this,
+ m_missingDictLang );
+ } else {
+ DictImportActivity
+ .downloadDictInBack( GamesList.this,
m_missingDictLang,
- m_missingDictNames[0],
+ m_missingDictName,
GamesList.this );
+ }
}
};
String message;
@@ -117,16 +127,20 @@ public class GamesList extends XWListActivity
if ( WARN_NODICT == id ) {
message = getString( R.string.no_dictf,
gameName, langName );
+ } else if ( WARN_NODICT_NEW == id ) {
+ message =
+ getString( R.string.invite_dict_missing_body_nonamef,
+ null, m_missingDictName, langName );
} else {
message = getString( R.string.no_dict_substf,
- gameName, m_missingDictNames[0],
+ gameName, m_missingDictName,
langName );
}
ab = new AlertDialog.Builder( this )
.setTitle( R.string.no_dict_title )
.setMessage( message )
- .setPositiveButton( R.string.button_ok, null )
+ .setPositiveButton( R.string.button_cancel, null )
.setNegativeButton( R.string.button_download, lstnr )
;
if ( WARN_NODICT_SUBST == id ) {
@@ -150,10 +164,12 @@ public class GamesList extends XWListActivity
getCheckedItemPosition();
String dict = m_sameLangDicts[pos];
dict = DictLangCache.stripCount( dict );
- GameUtils.replaceDicts( GamesList.this,
- m_missingDictRowId,
- m_missingDictNames[0],
- dict );
+ if ( GameUtils.replaceDicts( GamesList.this,
+ m_missingDictRowId,
+ m_missingDictName,
+ dict ) ) {
+ launchGameIf();
+ }
}
};
dialog = new AlertDialog.Builder( this )
@@ -184,8 +200,7 @@ public class GamesList extends XWListActivity
public void onClick( DialogInterface dlg, int item ) {
String name = namerView.getName();
DBUtils.setName( GamesList.this, m_rowid, name );
- m_adapter.inval( m_rowid );
- onContentChanged();
+ m_adapter.invalName( m_rowid );
}
};
dialog = new AlertDialog.Builder( this )
@@ -266,7 +281,9 @@ public class GamesList extends XWListActivity
}
});
- m_adapter = new GameListAdapter( this, new Handler(), this );
+ String field = CommonPrefs.getSummaryField( this );
+ m_adapter = new GameListAdapter( this, getListView(), new Handler(),
+ this, field );
setListAdapter( m_adapter );
NetUtils.informOfDeaths( this );
@@ -275,6 +292,7 @@ public class GamesList extends XWListActivity
startFirstHasDict( intent );
startNewNetGame( intent );
startHasGameID( intent );
+ startHasRowID( intent );
askDefaultNameIf();
} // onCreate
@@ -285,18 +303,17 @@ public class GamesList extends XWListActivity
{
super.onNewIntent( intent );
Assert.assertNotNull( intent );
- invalRelayIDs( intent.
- getStringArrayExtra( DispatchNotify.RELAYIDS_EXTRA ) );
+ invalRelayIDs( intent.getStringArrayExtra( RELAYIDS_EXTRA ) );
startFirstHasDict( intent );
startNewNetGame( intent );
startHasGameID( intent );
+ startHasRowID( intent );
}
@Override
protected void onStart()
{
super.onStart();
- DispatchNotify.SetRelayIDsHandler( this );
boolean hide = CommonPrefs.getHideIntro( this );
int hereOrGone = hide ? View.GONE : View.VISIBLE;
@@ -323,7 +340,6 @@ public class GamesList extends XWListActivity
// (TelephonyManager)getSystemService( Context.TELEPHONY_SERVICE );
// mgr.listen( m_phoneStateListener, PhoneStateListener.LISTEN_NONE );
// m_phoneStateListener = null;
- DispatchNotify.SetRelayIDsHandler( null );
super.onStop();
}
@@ -340,7 +356,7 @@ public class GamesList extends XWListActivity
{
super.onSaveInstanceState( outState );
outState.putLong( SAVE_ROWID, m_rowid );
- outState.putStringArray( SAVE_DICTNAMES, m_missingDictNames );
+ outState.putString( SAVE_DICTNAMES, m_missingDictName );
if ( null != m_netLaunchInfo ) {
m_netLaunchInfo.putSelf( outState );
}
@@ -351,7 +367,7 @@ public class GamesList extends XWListActivity
if ( null != bundle ) {
m_rowid = bundle.getLong( SAVE_ROWID );
m_netLaunchInfo = new NetLaunchInfo( bundle );
- m_missingDictNames = bundle.getStringArray( SAVE_DICTNAMES );
+ m_missingDictName = bundle.getString( SAVE_DICTNAMES );
}
}
@@ -364,62 +380,26 @@ public class GamesList extends XWListActivity
}
}
- // DispatchNotify.HandleRelaysIface interface
- public void handleRelaysIDs( final String[] relayIDs )
- {
- post( new Runnable() {
- public void run() {
- invalRelayIDs( relayIDs );
- startFirstHasDict( relayIDs );
- }
- } );
- }
-
- public void handleInvite( Uri invite )
- {
- final NetLaunchInfo nli = new NetLaunchInfo( invite );
- if ( nli.isValid() ) {
- post( new Runnable() {
- @Override
- public void run() {
- startNewNetGame( nli );
- }
- } );
- }
- }
-
- public void handleGameID( final int gameID )
- {
- post( new Runnable() {
- public void run() {
- startHasGameID( gameID );
- }
- } );
- }
-
// DBUtils.DBChangeListener interface
- public void gameSaved( final long rowid )
+ public void gameSaved( final long rowid, final boolean countChanged )
{
post( new Runnable() {
public void run() {
- m_adapter.inval( rowid );
- onContentChanged();
+ if ( countChanged ) {
+ onContentChanged();
+ } else {
+ m_adapter.inval( rowid );
+ }
}
} );
}
// GameListAdapter.LoadItemCB interface
- public void itemLoaded( long rowid )
- {
- onContentChanged();
- }
-
- public void itemClicked( long rowid )
+ public void itemClicked( long rowid, GameSummary summary )
{
// We need a way to let the user get back to the basic-config
// dialog in case it was dismissed. That way it to check for
// an empty room name.
- GameSummary summary = DBUtils.getSummary( this, rowid );
if ( summary.conType == CommsAddrRec.CommsConnType.COMMS_CONN_RELAY
&& summary.roomName.length() == 0 ) {
// If it's unconfigured and of the type RelayGameActivity
@@ -434,12 +414,12 @@ public class GamesList extends XWListActivity
GameUtils.doConfig( this, rowid, clazz );
} else {
if ( checkWarnNoDict( rowid ) ) {
- GameUtils.launchGame( this, rowid );
+ launchGame( rowid );
}
}
}
- // BTService.BTEventListener interface
+ // BTService.MultiEventListener interface
@Override
public void eventOccurred( MultiService.MultiEvent event,
final Object ... args )
@@ -466,11 +446,13 @@ public class GamesList extends XWListActivity
if ( AlertDialog.BUTTON_POSITIVE == which ) {
switch( id ) {
case NEW_NET_GAME_ACTION:
- long rowid = GameUtils.makeNewNetGame( this, m_netLaunchInfo );
- GameUtils.launchGame( this, rowid, true );
+ if ( checkWarnNoDict( m_netLaunchInfo ) ) {
+ makeNewNetGameIf();
+ }
break;
case RESET_GAME_ACTION:
GameUtils.resetGame( this, m_rowid );
+ onContentChanged(); // required because position may change
break;
case DELETE_GAME_ACTION:
GameUtils.deleteGame( this, m_rowid, true );
@@ -479,7 +461,6 @@ public class GamesList extends XWListActivity
long[] games = DBUtils.gamesList( this );
for ( int ii = games.length - 1; ii >= 0; --ii ) {
GameUtils.deleteGame( this, games[ii], ii == 0 );
- m_adapter.inval( games[ii] );
}
break;
case SYNC_MENU_ACTION:
@@ -616,14 +597,20 @@ public class GamesList extends XWListActivity
return handled;
}
- // NetUtils.DownloadFinishedListener interface
+ // DictImportActivity.DownloadFinishedListener interface
public void downloadFinished( String name, final boolean success )
{
post( new Runnable() {
public void run() {
- int id = success ? R.string.download_done
- : R.string.download_failed;
- Utils.showToast( GamesList.this, id );
+ boolean madeGame = false;
+ if ( success ) {
+ madeGame = makeNewNetGameIf() || launchGameIf();
+ }
+ if ( ! madeGame ) {
+ int id = success ? R.string.download_done
+ : R.string.download_failed;
+ Utils.showToast( GamesList.this, id );
+ }
}
} );
}
@@ -664,8 +651,7 @@ public class GamesList extends XWListActivity
showOKOnlyDialog( R.string.no_copy_network );
} else {
byte[] stream = GameUtils.savedGame( this, m_rowid );
- GameUtils.GameLock lock =
- GameUtils.saveNewGame( this, stream );
+ GameLock lock = GameUtils.saveNewGame( this, stream );
DBUtils.saveSummary( this, lock, summary );
lock.unlock();
}
@@ -690,21 +676,57 @@ public class GamesList extends XWListActivity
return handled;
} // handleMenuItem
+ private boolean checkWarnNoDict( NetLaunchInfo nli )
+ {
+ // check that we have the dict required
+ boolean haveDict;
+ if ( null == nli.dict ) { // can only test for language support
+ String[] dicts = DictLangCache.getHaveLang( this, nli.lang );
+ haveDict = 0 < dicts.length;
+ if ( haveDict ) {
+ // Just pick one -- good enough for the period when
+ // users aren't using new clients that include the
+ // dict name.
+ nli.dict = dicts[0];
+ }
+ } else {
+ haveDict =
+ DictLangCache.haveDict( this, nli.lang, nli.dict );
+ }
+ if ( !haveDict ) {
+ m_netLaunchInfo = nli;
+ m_missingDictLang = nli.lang;
+ m_missingDictName = nli.dict;
+ showDialog( WARN_NODICT_NEW );
+ }
+ return haveDict;
+ }
+
private boolean checkWarnNoDict( long rowid )
{
String[][] missingNames = new String[1][];
int[] missingLang = new int[1];
- boolean hasDicts = GameUtils.gameDictsHere( this, rowid,
- missingNames,
- missingLang );
+ boolean hasDicts =
+ GameUtils.gameDictsHere( this, rowid, missingNames, missingLang );
if ( !hasDicts ) {
- m_missingDictNames = missingNames[0];
m_missingDictLang = missingLang[0];
+ if ( 0 < missingNames[0].length ) {
+ m_missingDictName = missingNames[0][0];
+ } else {
+ m_missingDictName = null;
+ }
m_missingDictRowId = rowid;
if ( 0 == DictLangCache.getLangCount( this, m_missingDictLang ) ) {
showDialog( WARN_NODICT );
- } else {
+ } else if ( null != m_missingDictName ) {
showDialog( WARN_NODICT_SUBST );
+ } else {
+ String dict =
+ DictLangCache.getHaveLang( this, m_missingDictLang)[0];
+ if ( GameUtils.replaceDicts( this, m_missingDictRowId,
+ null, dict ) ) {
+ launchGameIf();
+ }
}
}
return hasDicts;
@@ -721,7 +743,6 @@ public class GamesList extends XWListActivity
}
}
}
- onContentChanged();
}
}
@@ -736,7 +757,7 @@ public class GamesList extends XWListActivity
if ( null != rowids ) {
for ( long rowid : rowids ) {
if ( GameUtils.gameDictsHere( this, rowid ) ) {
- GameUtils.launchGame( this, rowid );
+ launchGame( rowid );
break outer;
}
}
@@ -748,8 +769,7 @@ public class GamesList extends XWListActivity
private void startFirstHasDict( Intent intent )
{
if ( null != intent ) {
- String[] relayIDs =
- intent.getStringArrayExtra( DispatchNotify.RELAYIDS_EXTRA );
+ String[] relayIDs = intent.getStringArrayExtra( RELAYIDS_EXTRA );
startFirstHasDict( relayIDs );
}
}
@@ -759,47 +779,64 @@ public class GamesList extends XWListActivity
startActivity( new Intent( this, NewGameActivity.class ) );
}
- private void startNewNetGame( NetLaunchInfo info )
+ private void startNewNetGame( NetLaunchInfo nli )
{
- long rowid = DBUtils.getRowIDForOpen( this, info );
+ Date create = DBUtils.getMostRecentCreate( this, nli );
- if ( DBUtils.ROWID_NOTFOUND == rowid ) {
- rowid = GameUtils.makeNewNetGame( this, info );
- GameUtils.launchGame( this, rowid, true );
+ if ( null == create ) {
+ if ( checkWarnNoDict( nli ) ) {
+ makeNewNetGame( nli );
+ }
} else {
- String msg = getString( R.string.dup_game_queryf, info.room );
- m_netLaunchInfo = info;
+ String msg = getString( R.string.dup_game_queryf,
+ create.toString() );
+ m_netLaunchInfo = nli;
showConfirmThen( msg, NEW_NET_GAME_ACTION );
}
} // startNewNetGame
private void startNewNetGame( Intent intent )
{
- Uri data = intent.getData();
- if ( null != data ) {
- NetLaunchInfo info = new NetLaunchInfo( data );
- if ( info.isValid() ) {
- startNewNetGame( info );
+ NetLaunchInfo nli = null;
+ if ( MultiService.isMissingDictIntent( intent ) ) {
+ nli = new NetLaunchInfo( intent );
+ } else {
+ Uri data = intent.getData();
+ if ( null != data ) {
+ nli = new NetLaunchInfo( this, data );
}
}
+ if ( null != nli && nli.isValid() ) {
+ startNewNetGame( nli );
+ }
} // startNewNetGame
private void startHasGameID( int gameID )
{
long[] rowids = DBUtils.getRowIDsFor( this, gameID );
if ( null != rowids && 0 < rowids.length ) {
- GameUtils.launchGame( this, rowids[0] );
+ launchGame( rowids[0] );
}
}
private void startHasGameID( Intent intent )
{
- int gameID = intent.getIntExtra( DispatchNotify.GAMEID_EXTRA, 0 );
+ int gameID = intent.getIntExtra( GAMEID_EXTRA, 0 );
if ( 0 != gameID ) {
startHasGameID( gameID );
}
}
+ private void startHasRowID( Intent intent )
+ {
+ long rowid = intent.getLongExtra( REMATCH_ROWID_EXTRA, -1 );
+ if ( -1 != rowid ) {
+ // this will juggle if the preference is set
+ long newid = GameUtils.dupeGame( this, rowid );
+ launchGame( newid );
+ }
+ }
+
private void askDefaultNameIf()
{
if ( null == CommonPrefs.getDefaultPlayerName( this, 0, false ) ) {
@@ -812,10 +849,96 @@ public class GamesList extends XWListActivity
private void updateField()
{
String newField = CommonPrefs.getSummaryField( this );
- if ( ! newField.equals( m_nameField ) ) {
- m_nameField = newField;
- m_adapter.setField( newField );
+ if ( m_adapter.setField( newField ) ) {
+ // The adapter should be able to decide whether full
+ // content change is required. PENDING
onContentChanged();
}
}
+
+ private boolean makeNewNetGameIf()
+ {
+ boolean madeGame = null != m_netLaunchInfo;
+ if ( madeGame ) {
+ makeNewNetGame( m_netLaunchInfo );
+ m_netLaunchInfo = null;
+ }
+ return madeGame;
+ }
+
+ private boolean launchGameIf()
+ {
+ boolean madeGame = DBUtils.ROWID_NOTFOUND != m_missingDictRowId;
+ if ( madeGame ) {
+ GameUtils.launchGame( this, m_missingDictRowId );
+ m_missingDictRowId = DBUtils.ROWID_NOTFOUND;
+ }
+ return madeGame;
+ }
+
+ private void launchGame( long rowid, boolean invited )
+ {
+ GameUtils.launchGame( this, rowid, invited );
+ }
+
+ private void launchGame( long rowid )
+ {
+ launchGame( rowid, false );
+ }
+
+ private void makeNewNetGame( NetLaunchInfo info )
+ {
+ long rowid = GameUtils.makeNewNetGame( this, info );
+ launchGame( rowid, true );
+ }
+
+ public static void onGameDictDownload( Context context, Intent intent )
+ {
+ intent.setClass( context, GamesList.class );
+ context.startActivity( intent );
+ }
+
+ private static Intent makeSelfIntent( Context context )
+ {
+ Intent intent = new Intent( context, GamesList.class );
+ intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP
+ | Intent.FLAG_ACTIVITY_NEW_TASK );
+ return intent;
+ }
+
+ public static Intent makeRelayIdsIntent( Context context,
+ String[] relayIDs )
+ {
+ Intent intent = makeSelfIntent( context );
+ intent.putExtra( RELAYIDS_EXTRA, relayIDs );
+ return intent;
+ }
+
+ public static Intent makeGameIDIntent( Context context, int gameID )
+ {
+ Intent intent = makeSelfIntent( context );
+ intent.putExtra( GAMEID_EXTRA, gameID );
+ return intent;
+ }
+
+ public static Intent makeRematchIntent( Context context, CurGameInfo gi,
+ long rowid )
+ {
+ Intent intent = makeSelfIntent( context );
+
+ if ( CurGameInfo.DeviceRole.SERVER_STANDALONE == gi.serverRole ) {
+ intent.putExtra( REMATCH_ROWID_EXTRA, rowid );
+ } else {
+ Utils.notImpl( context );
+ }
+
+ return intent;
+ }
+
+ public static void openGame( Context context, Uri data )
+ {
+ Intent intent = makeSelfIntent( context );
+ intent.setData( data );
+ context.startActivity( intent );
+ }
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/InviteActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/InviteActivity.java
index 1a5c803f0..41dd2ae7c 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/InviteActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/InviteActivity.java
@@ -43,7 +43,7 @@ abstract class InviteActivity extends XWListActivity
implements View.OnClickListener {
public static final String DEVS = "DEVS";
- public static final String INTENT_KEY_NMISSING = "NMISSING";
+ protected static final String INTENT_KEY_NMISSING = "NMISSING";
protected int m_nMissing;
protected Button m_okButton;
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/MultiService.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/MultiService.java
index 8c06598b0..5d2a776eb 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/MultiService.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/MultiService.java
@@ -31,6 +31,8 @@ public class MultiService {
public static final String LANG = "LANG";
public static final String DICT = "DICT";
public static final String GAMEID = "GAMEID";
+ public static final String INVITEID = "INVITEID"; // relay only
+ public static final String ROOM = "ROOM";
public static final String GAMENAME = "GAMENAME";
public static final String NPLAYERST = "NPLAYERST";
public static final String NPLAYERSH = "NPLAYERSH";
@@ -38,8 +40,9 @@ public class MultiService {
public static final String OWNER = "OWNER";
public static final int OWNER_SMS = 1;
+ public static final int OWNER_RELAY = 2;
- private BTEventListener m_li;
+ private MultiEventListener m_li;
public enum MultiEvent { BAD_PROTO
, BT_ENABLED
@@ -61,14 +64,14 @@ public class MultiService {
, SMS_SEND_FAILED_NORADIO
};
- public interface BTEventListener {
+ public interface MultiEventListener {
public void eventOccurred( MultiEvent event, Object ... args );
}
// public interface MultiEventSrc {
// public void setBTEventListener( BTEventListener li );
// }
- public void setListener( BTEventListener li )
+ public void setListener( MultiEventListener li )
{
synchronized( this ) {
m_li = li;
@@ -84,11 +87,39 @@ public class MultiService {
}
}
+ public static void fillInviteIntent( Intent intent, String gameName,
+ int lang, String dict,
+ int nPlayersT, int nPlayersH )
+ {
+ intent.putExtra( GAMENAME, gameName );
+ intent.putExtra( LANG, lang );
+ intent.putExtra( DICT, dict );
+ intent.putExtra( NPLAYERST, nPlayersT ); // both of these used
+ intent.putExtra( NPLAYERSH, nPlayersH );
+ }
+
+ public static Intent makeMissingDictIntent( Context context, String gameName,
+ int lang, String dict,
+ int nPlayersT, int nPlayersH )
+ {
+ Intent intent = new Intent( context, DictsActivity.class );
+ fillInviteIntent( intent, gameName, lang, dict, nPlayersT, nPlayersH );
+ return intent;
+ }
+
+ public static Intent makeMissingDictIntent( Context context, NetLaunchInfo nli )
+ {
+ Intent intent = makeMissingDictIntent( context, null, nli.lang, nli.dict,
+ nli.nPlayersT, 1 );
+ intent.putExtra( ROOM, nli.room );
+ return intent;
+ }
+
public static boolean isMissingDictIntent( Intent intent )
{
return intent.hasExtra( LANG )
- && intent.hasExtra( DICT )
- && intent.hasExtra( GAMEID )
+ // && intent.hasExtra( DICT )
+ && (intent.hasExtra( GAMEID ) || intent.hasExtra( ROOM ))
&& intent.hasExtra( GAMENAME )
&& intent.hasExtra( NPLAYERST )
&& intent.hasExtra( NPLAYERSH );
@@ -102,8 +133,10 @@ public class MultiService {
String langStr = DictLangCache.getLangName( context, lang );
String dict = intent.getStringExtra( DICT );
String inviter = intent.getStringExtra( INVITER );
- String msg = context.getString( R.string.invite_dict_missing_bodyf,
- inviter, dict, langStr );
+ int msgID = (null == inviter) ? R.string.invite_dict_missing_body_nonamef
+ : R.string.invite_dict_missing_bodyf;
+ String msg = context.getString( msgID, inviter, dict, langStr );
+
return new AlertDialog.Builder( context )
.setTitle( R.string.invite_dict_missing_title )
.setMessage( msg)
@@ -112,6 +145,13 @@ public class MultiService {
.create();
}
+ public static void postMissingDictNotification( Context content,
+ Intent intent, int id )
+ {
+ Utils.postNotification( content, intent, R.string.missing_dict_title,
+ R.string.missing_dict_detail, id );
+ }
+
// resend the intent, but only if the dict it names is here. (If
// it's not, we may need to try again later, e.g. because our cue
// was a focus gain.)
@@ -123,11 +163,15 @@ public class MultiService {
String dict = intent.getStringExtra( DICT );
downloaded = DictLangCache.haveDict( context, lang, dict );
if ( downloaded ) {
- int owner = intent.getIntExtra( OWNER, -1 );
- if ( owner == OWNER_SMS ) {
+ switch ( intent.getIntExtra( OWNER, -1 ) ) {
+ case OWNER_SMS:
SMSService.onGameDictDownload( context, intent );
- } else {
- DbgUtils.logf( "unexpected OWNER: %d", owner );
+ break;
+ case OWNER_RELAY:
+ GamesList.onGameDictDownload( context, intent );
+ break;
+ default:
+ DbgUtils.logf( "unexpected OWNER" );
}
}
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetLaunchInfo.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetLaunchInfo.java
index 5e6c08b4b..878316cf1 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetLaunchInfo.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetLaunchInfo.java
@@ -20,21 +20,27 @@
package org.eehouse.android.xw4;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
-import android.net.Uri.Builder;
import android.os.Bundle;
import java.net.URLEncoder;
+import java.io.InputStream;
+import org.json.JSONObject;
+import junit.framework.Assert;
public class NetLaunchInfo {
public String room;
public String inviteID;
+ public String dict;
public int lang;
- public int nPlayers;
+ public int nPlayersT;
private static final String LANG = "netlaunchinfo_lang";
private static final String ROOM = "netlaunchinfo_room";
+ private static final String DICT = "netlaunchinfo_dict";
private static final String INVITEID = "netlaunchinfo_inviteid";
private static final String NPLAYERS = "netlaunchinfo_nplayers";
private static final String VALID = "netlaunchinfo_valid";
@@ -46,30 +52,50 @@ public class NetLaunchInfo {
bundle.putInt( LANG, lang );
bundle.putString( ROOM, room );
bundle.putString( INVITEID, inviteID );
- bundle.putInt( NPLAYERS, nPlayers );
+ bundle.putString( DICT, dict );
+ bundle.putInt( NPLAYERS, nPlayersT );
bundle.putBoolean( VALID, m_valid );
}
public NetLaunchInfo( Bundle bundle )
{
- lang = bundle.getInt( LANG );
+ lang = bundle.getInt( LANG );
room = bundle.getString( ROOM );
+ dict = bundle.getString( DICT );
inviteID = bundle.getString( INVITEID );
- nPlayers = bundle.getInt( NPLAYERS );
- m_valid = bundle.getBoolean( VALID );
+ nPlayersT = bundle.getInt( NPLAYERS );
+ m_valid = bundle.getBoolean( VALID );
}
- public NetLaunchInfo( Uri data )
+ public NetLaunchInfo( Context context, Uri data )
{
m_valid = false;
if ( null != data ) {
+ String scheme = data.getScheme();
try {
- room = data.getQueryParameter( "room" );
- inviteID = data.getQueryParameter( "id" );
- String langStr = data.getQueryParameter( "lang" );
- lang = Integer.decode( langStr );
- String np = data.getQueryParameter( "np" );
- nPlayers = Integer.decode( np );
+ if ( "content".equals(scheme) || "file".equals(scheme) ) {
+ Assert.assertNotNull( context );
+ ContentResolver resolver = context.getContentResolver();
+ InputStream is = resolver.openInputStream( data );
+ int len = is.available();
+ byte[] buf = new byte[len];
+ is.read( buf );
+
+ JSONObject json = new JSONObject( new String( buf ) );
+ room = json.getString( MultiService.ROOM );
+ inviteID = json.getString( MultiService.INVITEID );
+ lang = json.getInt( MultiService.LANG );
+ dict = json.getString( MultiService.DICT );
+ nPlayersT = json.getInt( MultiService.NPLAYERST );
+ } else {
+ room = data.getQueryParameter( "room" );
+ inviteID = data.getQueryParameter( "id" );
+ dict = data.getQueryParameter( "wl" );
+ String langStr = data.getQueryParameter( "lang" );
+ lang = Integer.decode( langStr );
+ String np = data.getQueryParameter( "np" );
+ nPlayersT = Integer.decode( np );
+ }
m_valid = true;
} catch ( Exception e ) {
DbgUtils.logf( "unable to parse \"%s\"", data.toString() );
@@ -77,18 +103,34 @@ public class NetLaunchInfo {
}
}
- public static Uri makeLaunchUri( Context context, String room,
- String inviteID, int lang, int nPlayers )
+ public NetLaunchInfo( Intent intent )
{
- Builder ub = new Builder();
- ub.scheme( "http" );
- ub.path( context.getString( R.string.game_url_pathf,
- XWPrefs.getDefaultRedirHost( context ) ) );
-
- ub.appendQueryParameter( "lang", String.format("%d", lang ) );
- ub.appendQueryParameter( "np", String.format( "%d", nPlayers ) );
- ub.appendQueryParameter( "room", room );
- ub.appendQueryParameter( "id", inviteID );
+ room = intent.getStringExtra( MultiService.ROOM );
+ inviteID = intent.getStringExtra( MultiService.INVITEID );
+ lang = intent.getIntExtra( MultiService.LANG, -1 );
+ dict = intent.getStringExtra( MultiService.DICT );
+ nPlayersT = intent.getIntExtra( MultiService.NPLAYERST, -1 );
+ m_valid = null != room
+ && -1 != lang
+ && -1 != nPlayersT;
+ }
+
+ public static Uri makeLaunchUri( Context context, String room,
+ String inviteID, int lang,
+ String dict, int nPlayersT )
+ {
+ Uri.Builder ub = new Uri.Builder()
+ .scheme( "http" )
+ .path( String.format( "//%s%s",
+ context.getString(R.string.invite_host),
+ context.getString(R.string.invite_prefix) ) )
+ .appendQueryParameter( "lang", String.format("%d", lang ) )
+ .appendQueryParameter( "np", String.format( "%d", nPlayersT ) )
+ .appendQueryParameter( "room", room )
+ .appendQueryParameter( "id", inviteID );
+ if ( null != dict ) {
+ ub.appendQueryParameter( "wl", dict );
+ }
return ub.build();
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetUtils.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetUtils.java
index 288a935e6..4e8ee9942 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetUtils.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetUtils.java
@@ -21,27 +21,14 @@
package org.eehouse.android.xw4;
import android.content.Context;
-import android.os.Handler;
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.Socket;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
import javax.net.SocketFactory;
public class NetUtils {
- private static final int MAX_SEND = 1024;
- private static final int MAX_BUF = MAX_SEND - 2;
-
public static final byte PROTOCOL_VERSION = 0;
// from xwrelay.h
public static byte PRX_PUB_ROOMS = 1;
@@ -50,10 +37,6 @@ public class NetUtils {
public static byte PRX_GET_MSGS = 4;
public static byte PRX_PUT_MSGS = 5;
- public interface DownloadFinishedListener {
- void downloadFinished( String name, boolean success );
- }
-
public static Socket makeProxySocket( Context context,
int timeoutMillis )
{
@@ -140,8 +123,7 @@ public class NetUtils {
}
}
- public static byte[][][] queryRelay( Context context, String[] ids,
- int nBytes )
+ public static byte[][][] queryRelay( Context context, String[] ids )
{
byte[][][] msgs = null;
try {
@@ -151,6 +133,7 @@ public class NetUtils {
new DataOutputStream( socket.getOutputStream() );
// total packet size
+ int nBytes = sumStrings( ids );
outStream.writeShort( 2 + nBytes + ids.length + 1 );
outStream.writeByte( NetUtils.PROTOCOL_VERSION );
@@ -204,132 +187,15 @@ public class NetUtils {
return msgs;
} // queryRelay
- public static void sendToRelay( Context context,
- HashMap> msgHash )
+ private static int sumStrings( final String[] strs )
{
- // format: total msg lenth: 2
- // number-of-relayIDs: 2
- // for-each-relayid: relayid + '\n': varies
- // message count: 1
- // for-each-message: length: 2
- // message: varies
-
- if ( null != msgHash ) {
- try {
- // Build up a buffer containing everything but the total
- // message length and number of relayIDs in the message.
- ByteArrayOutputStream store =
- new ByteArrayOutputStream( MAX_BUF ); // mem
- DataOutputStream outBuf = new DataOutputStream( store );
- int msgLen = 4; // relayID count + protocol stuff
- int nRelayIDs = 0;
-
- Iterator iter = msgHash.keySet().iterator();
- while ( iter.hasNext() ) {
- String relayID = iter.next();
- int thisLen = 1 + relayID.length(); // string and '\n'
- thisLen += 2; // message count
-
- ArrayList msgs = msgHash.get( relayID );
- for ( byte[] msg : msgs ) {
- thisLen += 2 + msg.length;
- }
-
- if ( msgLen + thisLen > MAX_BUF ) {
- // Need to deal with this case by sending multiple
- // packets. It WILL happen.
- break;
- }
- // got space; now write it
- ++nRelayIDs;
- outBuf.writeBytes( relayID );
- outBuf.write( '\n' );
- outBuf.writeShort( msgs.size() );
- for ( byte[] msg : msgs ) {
- outBuf.writeShort( msg.length );
- outBuf.write( msg );
- }
- msgLen += thisLen;
- }
-
- // Now open a real socket, write size and proto, and
- // copy in the formatted buffer
- Socket socket = makeProxySocket( context, 8000 );
- if ( null != socket ) {
- DataOutputStream outStream =
- new DataOutputStream( socket.getOutputStream() );
- outStream.writeShort( msgLen );
- outStream.writeByte( NetUtils.PROTOCOL_VERSION );
- outStream.writeByte( NetUtils.PRX_PUT_MSGS );
- outStream.writeShort( nRelayIDs );
- outStream.write( store.toByteArray() );
- outStream.flush();
- socket.close();
- }
- } catch ( java.io.IOException ioe ) {
- DbgUtils.loge( ioe );
+ int len = 0;
+ if ( null != strs ) {
+ for ( String str : strs ) {
+ len += str.length();
}
- } else {
- DbgUtils.logf( "sendToRelay: null msgs" );
}
- } // sendToRelay
-
- static void downloadDictInBack( Context context, int lang, String name,
- DownloadFinishedListener lstnr )
- {
- DictUtils.DictLoc loc = XWPrefs.getDefaultLoc( context );
- downloadDictInBack( context, lang, name, loc, lstnr );
- }
-
- static void downloadDictInBack( Context context, int lang, String name,
- DictUtils.DictLoc loc,
- DownloadFinishedListener lstnr )
- {
- String url = Utils.makeDictUrl( context, lang, name );
- downloadDictInBack( context, url, loc, lstnr );
- }
-
- static void downloadDictInBack( final Context context, final String urlStr,
- final DictUtils.DictLoc loc,
- final DownloadFinishedListener lstnr )
- {
- String tmp = Utils.dictFromURL( context, urlStr );
- final String name = DictUtils.removeDictExtn( tmp );
- String msg = context.getString( R.string.downloadingf, name );
- final StatusNotifier sno =
- new StatusNotifier( context, msg, R.string.download_done );
-
- new Thread( new Runnable() {
- public void run() {
- boolean success = false;
- HttpURLConnection urlConn = null;
- try {
- URL url = new URL( urlStr );
- urlConn = (HttpURLConnection)url.openConnection();
- InputStream in = new
- BufferedInputStream( urlConn.getInputStream(),
- 1024*8 );
- success = DictUtils.saveDict( context, in,
- name, loc );
- DbgUtils.logf( "saveDict returned %b", success );
-
- } catch ( java.net.MalformedURLException mue ) {
- DbgUtils.loge( mue );
- } catch ( java.io.IOException ioe ) {
- DbgUtils.loge( ioe );
- } finally {
- if ( null != urlConn ) {
- urlConn.disconnect();
- }
- }
-
- sno.close();
- DictLangCache.inval( context, name, loc, true );
- if ( null != lstnr ) {
- lstnr.downloadFinished( name, success );
- }
- }
- } ).start();
+ return len;
}
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java
index 3cfee37c9..d1b90148a 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java
@@ -256,7 +256,7 @@ public class NewGameActivity extends XWActivity {
return dialog;
}
- // BTService.BTEventListener interface
+ // MultiService.MultiEventListener interface
@Override
public void eventOccurred( MultiService.MultiEvent event,
final Object ... args )
@@ -299,7 +299,7 @@ public class NewGameActivity extends XWActivity {
super.eventOccurred( event, args );
break;
}
- } // BTService.BTEventListener.eventOccurred
+ } // MultiService.MultiEventListener.eventOccurred
private void makeNewGame( boolean networked, boolean launch )
{
@@ -318,13 +318,14 @@ public class NewGameActivity extends XWActivity {
String inviteID = null;
long rowid;
int[] lang = {0};
+ String[] dict = {null};
final int nPlayers = 2; // hard-coded for no-configure case
if ( networked ) {
room = GameUtils.makeRandomID();
inviteID = GameUtils.makeRandomID();
rowid = GameUtils.makeNewNetGame( this, room, inviteID, lang,
- nPlayers, 1 );
+ dict, nPlayers, 1 );
} else {
rowid = GameUtils.saveNew( this, new CurGameInfo( this ) );
}
@@ -333,7 +334,8 @@ public class NewGameActivity extends XWActivity {
GameUtils.launchGame( this, rowid, networked );
if ( networked ) {
GameUtils.launchInviteActivity( this, choseEmail, room,
- inviteID, lang[0], nPlayers );
+ inviteID, lang[0], dict[0],
+ nPlayers );
}
} else {
GameUtils.doConfig( this, rowid, GameConfig.class );
@@ -355,7 +357,7 @@ public class NewGameActivity extends XWActivity {
intent.putExtra( GameUtils.INTENT_FORRESULT_ROWID, true );
startActivityForResult( intent, CONFIG_FOR_BT );
} else {
- GameUtils.launchBTInviter( this, 1, INVITE_FOR_BT );
+ BTInviteActivity.launchForResult( this, 1, INVITE_FOR_BT );
}
}
@@ -376,7 +378,7 @@ public class NewGameActivity extends XWActivity {
intent.putExtra( GameUtils.INTENT_FORRESULT_ROWID, true );
startActivityForResult( intent, CONFIG_FOR_SMS );
} else {
- GameUtils.launchSMSInviter( this, 1, INVITE_FOR_SMS );
+ SMSInviteActivity.launchForResult( this, 1, INVITE_FOR_SMS );
}
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayGameActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayGameActivity.java
index e5571a6a1..16ead9288 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayGameActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayGameActivity.java
@@ -41,7 +41,7 @@ public class RelayGameActivity extends XWActivity
private long m_rowid;
private CurGameInfo m_gi;
- private GameUtils.GameLock m_gameLock;
+ private GameLock m_gameLock;
private CommsAddrRec m_car;
private Button m_playButton;
private Button m_configButton;
@@ -68,22 +68,28 @@ public class RelayGameActivity extends XWActivity
super.onStart();
m_gi = new CurGameInfo( this );
- m_gameLock = new GameUtils.GameLock( m_rowid, true ).lock();
- int gamePtr = GameUtils.loadMakeGame( this, m_gi, m_gameLock );
- m_car = new CommsAddrRec();
- if ( XwJNI.game_hasComms( gamePtr ) ) {
- XwJNI.comms_getAddr( gamePtr, m_car );
+ m_gameLock = new GameLock( m_rowid, true ).lock( 300 );
+ if ( null == m_gameLock ) {
+ DbgUtils.logf( "RelayGameActivity.onStart(): unable to lock rowid %d",
+ m_rowid );
+ finish();
} else {
- Assert.fail();
- // String relayName = CommonPrefs.getDefaultRelayHost( this );
- // int relayPort = CommonPrefs.getDefaultRelayPort( this );
- // XwJNI.comms_getInitialAddr( m_carOrig, relayName, relayPort );
- }
- XwJNI.game_dispose( gamePtr );
+ int gamePtr = GameUtils.loadMakeGame( this, m_gi, m_gameLock );
+ m_car = new CommsAddrRec();
+ if ( XwJNI.game_hasComms( gamePtr ) ) {
+ XwJNI.comms_getAddr( gamePtr, m_car );
+ } else {
+ Assert.fail();
+ // String relayName = CommonPrefs.getDefaultRelayHost( this );
+ // int relayPort = CommonPrefs.getDefaultRelayPort( this );
+ // XwJNI.comms_getInitialAddr( m_carOrig, relayName, relayPort );
+ }
+ XwJNI.game_dispose( gamePtr );
- String lang = DictLangCache.getLangName( this, m_gi.dictLang );
- TextView text = (TextView)findViewById( R.id.explain );
- text.setText( getString( R.string.relay_game_explainf, lang ) );
+ String lang = DictLangCache.getLangName( this, m_gi.dictLang );
+ TextView text = (TextView)findViewById( R.id.explain );
+ text.setText( getString( R.string.relay_game_explainf, lang ) );
+ }
}
@Override
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayMsgSink.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayMsgSink.java
deleted file mode 100644
index 53e0ccfdd..000000000
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayMsgSink.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/* -*- compile-command: "cd ../../../../../; ant install"; -*- */
-/*
- * Copyright 2009-2010 by Eric House (xwords@eehouse.org). All
- * rights reserved.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU General Public License as
- * published by the Free Software Foundation; either version 2 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- */
-
-package org.eehouse.android.xw4;
-
-import android.content.Context;
-import java.util.HashMap;
-import java.util.ArrayList;
-
-import junit.framework.Assert;
-
-import org.eehouse.android.xw4.jni.*;
-
-public class RelayMsgSink extends MultiMsgSink {
-
- private HashMap> m_msgLists = null;
-
- public void send( Context context )
- {
- NetUtils.sendToRelay( context, m_msgLists );
- }
-
- /***** TransportProcs interface *****/
-
- public boolean relayNoConnProc( byte[] buf, String relayID )
- {
- if ( null == m_msgLists ) {
- m_msgLists = new HashMap>();
- }
-
- ArrayList list = m_msgLists.get( relayID );
- if ( list == null ) {
- list = new ArrayList();
- m_msgLists.put( relayID, list );
- }
- list.add( buf );
-
- return true;
- }
-}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayService.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayService.java
index 76fc3c1f8..bc4d2d179 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayService.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayService.java
@@ -24,18 +24,18 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
-import javax.net.SocketFactory;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.io.InputStream;
-import java.io.DataInputStream;
-import java.io.OutputStream;
+import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
+import java.net.Socket;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
import org.eehouse.android.xw4.jni.GameSummary;
public class RelayService extends Service {
+ private static final int MAX_SEND = 1024;
+ private static final int MAX_BUF = MAX_SEND - 2;
@Override
public void onCreate()
@@ -63,62 +63,153 @@ public class RelayService extends Service {
long[] rowids = DBUtils.getRowIDsFor( this, relayID );
if ( null != rowids ) {
for ( long rowid : rowids ) {
- Intent intent = new Intent( this, DispatchNotify.class );
- intent.putExtra( DispatchNotify.RELAYIDS_EXTRA,
- new String[] {relayID} );
+ Intent intent =
+ GamesList.makeRelayIdsIntent( this,
+ new String[] {relayID} );
String msg = Utils.format( this, R.string.notify_bodyf,
GameUtils.getName( this, rowid ) );
Utils.postNotification( this, intent, R.string.notify_title,
- msg, relayID.hashCode() );
+ msg, (int)rowid );
}
}
}
}
-
- private String[] collectIDs( int[] nBytes )
- {
- String[] ids = DBUtils.getRelayIDs( this, false );
- int len = 0;
- if ( null != ids ) {
- for ( String id : ids ) {
- len += id.length();
- }
- }
- nBytes[0] = len;
- return ids;
- }
private void fetchAndProcess()
{
- int[] nBytes = new int[1];
- String[] ids = collectIDs( nBytes );
- if ( null != ids && 0 < ids.length ) {
- RelayMsgSink sink = new RelayMsgSink();
- byte[][][] msgs =
- NetUtils.queryRelay( this, ids, nBytes[0] );
+ long[][] rowIDss = new long[1][];
+ String[] relayIDs = DBUtils.getRelayIDs( this, rowIDss );
+ if ( null != relayIDs && 0 < relayIDs.length ) {
+ long[] rowIDs = rowIDss[0];
+ byte[][][] msgs = NetUtils.queryRelay( this, relayIDs );
if ( null != msgs ) {
- int nameCount = ids.length;
+ RelayMsgSink sink = new RelayMsgSink();
+ int nameCount = relayIDs.length;
ArrayList idsWMsgs =
new ArrayList( nameCount );
for ( int ii = 0; ii < nameCount; ++ii ) {
+ byte[][] forOne = msgs[ii];
// if game has messages, open it and feed 'em
// to it.
- if ( GameUtils.feedMessages( this, ids[ii],
- msgs[ii], sink ) ) {
- idsWMsgs.add( ids[ii] );
+ if ( null == forOne ) {
+ // Nothing for this relayID
+ } else if ( BoardActivity.feedMessages( rowIDs[ii], forOne )
+ || GameUtils.feedMessages( this, rowIDs[ii],
+ forOne, null,
+ sink ) ) {
+ idsWMsgs.add( relayIDs[ii] );
+ } else {
+ DbgUtils.logf( "dropping message for %s (rowid %d)",
+ relayIDs[ii], rowIDs[ii] );
}
}
if ( 0 < idsWMsgs.size() ) {
- String[] relayIDs = new String[idsWMsgs.size()];
- idsWMsgs.toArray( relayIDs );
- if ( !DispatchNotify.tryHandle( relayIDs ) ) {
- setupNotification( relayIDs );
- }
+ String[] tmp = new String[idsWMsgs.size()];
+ idsWMsgs.toArray( tmp );
+ setupNotification( tmp );
}
sink.send( this );
}
}
}
+ private static void sendToRelay( Context context,
+ HashMap> msgHash )
+ {
+ // format: total msg lenth: 2
+ // number-of-relayIDs: 2
+ // for-each-relayid: relayid + '\n': varies
+ // message count: 1
+ // for-each-message: length: 2
+ // message: varies
+
+ if ( null != msgHash ) {
+ try {
+ // Build up a buffer containing everything but the total
+ // message length and number of relayIDs in the message.
+ ByteArrayOutputStream store =
+ new ByteArrayOutputStream( MAX_BUF ); // mem
+ DataOutputStream outBuf = new DataOutputStream( store );
+ int msgLen = 4; // relayID count + protocol stuff
+ int nRelayIDs = 0;
+
+ Iterator iter = msgHash.keySet().iterator();
+ while ( iter.hasNext() ) {
+ String relayID = iter.next();
+ int thisLen = 1 + relayID.length(); // string and '\n'
+ thisLen += 2; // message count
+
+ ArrayList msgs = msgHash.get( relayID );
+ for ( byte[] msg : msgs ) {
+ thisLen += 2 + msg.length;
+ }
+
+ if ( msgLen + thisLen > MAX_BUF ) {
+ // Need to deal with this case by sending multiple
+ // packets. It WILL happen.
+ break;
+ }
+ // got space; now write it
+ ++nRelayIDs;
+ outBuf.writeBytes( relayID );
+ outBuf.write( '\n' );
+ outBuf.writeShort( msgs.size() );
+ for ( byte[] msg : msgs ) {
+ outBuf.writeShort( msg.length );
+ outBuf.write( msg );
+ }
+ msgLen += thisLen;
+ }
+
+ // Now open a real socket, write size and proto, and
+ // copy in the formatted buffer
+ Socket socket = NetUtils.makeProxySocket( context, 8000 );
+ if ( null != socket ) {
+ DataOutputStream outStream =
+ new DataOutputStream( socket.getOutputStream() );
+ outStream.writeShort( msgLen );
+ outStream.writeByte( NetUtils.PROTOCOL_VERSION );
+ outStream.writeByte( NetUtils.PRX_PUT_MSGS );
+ outStream.writeShort( nRelayIDs );
+ outStream.write( store.toByteArray() );
+ outStream.flush();
+ socket.close();
+ }
+ } catch ( java.io.IOException ioe ) {
+ DbgUtils.loge( ioe );
+ }
+ } else {
+ DbgUtils.logf( "sendToRelay: null msgs" );
+ }
+ } // sendToRelay
+
+ private class RelayMsgSink extends MultiMsgSink {
+
+ private HashMap> m_msgLists = null;
+
+ public void send( Context context )
+ {
+ sendToRelay( context, m_msgLists );
+ }
+
+ /***** TransportProcs interface *****/
+
+ public boolean relayNoConnProc( byte[] buf, String relayID )
+ {
+ if ( null == m_msgLists ) {
+ m_msgLists = new HashMap>();
+ }
+
+ ArrayList list = m_msgLists.get( relayID );
+ if ( list == null ) {
+ list = new ArrayList();
+ m_msgLists.put( relayID, list );
+ }
+ list.add( buf );
+
+ return true;
+ }
+ }
+
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSInviteActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSInviteActivity.java
index 60a7da3dd..aa955374b 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSInviteActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSInviteActivity.java
@@ -32,9 +32,9 @@ import android.os.Bundle;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract;
-import android.text.method.DialerKeyListener;
import android.text.Editable;
import android.text.TextWatcher;
+import android.text.method.DialerKeyListener;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
@@ -64,6 +64,14 @@ public class SMSInviteActivity extends InviteActivity {
private String m_pendingNumber;
private boolean m_immobileConfirmed;
+ public static void launchForResult( Activity activity, int nMissing,
+ int requestCode )
+ {
+ Intent intent = new Intent( activity, SMSInviteActivity.class );
+ intent.putExtra( INTENT_KEY_NMISSING, nMissing );
+ activity.startActivityForResult( intent, requestCode );
+ }
+
@Override
protected void onCreate( Bundle savedInstanceState )
{
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSService.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSService.java
index 6121e7196..be0a95ef2 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSService.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/SMSService.java
@@ -189,7 +189,7 @@ public class SMSService extends Service {
return result;
}
- public static void setListener( MultiService.BTEventListener li )
+ public static void setListener( MultiService.MultiEventListener li )
{
if ( XWApp.SMSSUPPORTED ) {
if ( null == s_srcMgr ) {
@@ -388,7 +388,6 @@ public class SMSService extends Service {
int count = (msg.length() + (MAX_LEN_TEXT-1)) / MAX_LEN_TEXT;
String[] result = new String[count];
int msgID = ++s_nSent % 0x000000FF;
- DbgUtils.logf( "preparing %d packets for msgid %x", count, msgID );
int start = 0;
int end = 0;
@@ -400,7 +399,6 @@ public class SMSService extends Service {
end += len;
result[ii] = String.format( "0:%X:%X:%X:%s", msgID, ii, count,
msg.substring( start, end ) );
- DbgUtils.logf( "fragment[%d]: %s", ii, result[ii] );
start = end;
}
return result;
@@ -424,17 +422,16 @@ public class SMSService extends Service {
makeForInvite( phone, gameID, gameName, lang, dict,
nPlayersT, nPlayersH );
} else {
- Intent intent = new Intent( this, DictsActivity.class );
- fillInviteIntent( intent, phone, gameID, gameName, lang, dict,
- nPlayersT, nPlayersH );
+ Intent intent = MultiService
+ .makeMissingDictIntent( this, gameName, lang, dict,
+ nPlayersT, nPlayersH );
+ intent.putExtra( PHONE, phone );
intent.putExtra( MultiService.OWNER,
MultiService.OWNER_SMS );
intent.putExtra( MultiService.INVITER,
Utils.phoneToContact( this, phone, true ) );
- Utils.postNotification( this, intent,
- R.string.missing_dict_title,
- R.string.missing_dict_detail,
- gameID );
+ MultiService.postMissingDictNotification( this, intent,
+ gameID );
}
break;
case DATA:
@@ -506,7 +503,6 @@ public class SMSService extends Service {
private void disAssemble( String senderPhone, String fullMsg )
{
- DbgUtils.logf( "disAssemble()" );
byte[] data = XwJNI.base64Decode( fullMsg );
DataInputStream dis =
new DataInputStream( new ByteArrayInputStream(data) );
@@ -544,7 +540,7 @@ public class SMSService extends Service {
String owner = Utils.phoneToContact( this, phone, true );
String body = Utils.format( this, R.string.new_name_bodyf,
owner );
- postNotification( gameID, R.string.new_sms_title, body );
+ postNotification( gameID, R.string.new_sms_title, body, rowid );
ackInvite( phone, gameID );
}
@@ -565,8 +561,6 @@ public class SMSService extends Service {
for ( String fragment : fragments ) {
String asPublic = toPublicFmt( fragment );
mgr.sendTextMessage( phone, null, asPublic, sent, delivery );
- DbgUtils.logf( "Message \"%s\" of %d bytes sent to %s.",
- asPublic, asPublic.length(), phone );
}
if ( s_showToasts ) {
DbgUtils.showf( this, "sent %dth msg", s_nSent );
@@ -591,11 +585,8 @@ public class SMSService extends Service {
{
intent.putExtra( PHONE, phone );
intent.putExtra( MultiService.GAMEID, gameID );
- intent.putExtra( MultiService.GAMENAME, gameName );
- intent.putExtra( MultiService.LANG, lang );
- intent.putExtra( MultiService.DICT, dict );
- intent.putExtra( MultiService.NPLAYERST, nPlayersT );
- intent.putExtra( MultiService.NPLAYERSH, nPlayersH );
+ MultiService.fillInviteIntent( intent, gameName, lang, dict,
+ nPlayersT, nPlayersH );
}
private void feedMessage( int gameID, byte[] msg, CommsAddrRec addr )
@@ -612,19 +603,19 @@ public class SMSService extends Service {
if ( GameUtils.feedMessage( this, rowid, msg, addr,
sink ) ) {
postNotification( gameID, R.string.new_smsmove_title,
- getString(R.string.new_move_body)
- );
+ getString(R.string.new_move_body),
+ rowid );
}
}
}
}
}
- private void postNotification( int gameID, int title, String body )
+ private void postNotification( int gameID, int title, String body,
+ long rowid )
{
- Intent intent = new Intent( this, DispatchNotify.class );
- intent.putExtra( DispatchNotify.GAMEID_EXTRA, gameID );
- Utils.postNotification( this, intent, title, body, gameID );
+ Intent intent = GamesList.makeGameIDIntent( this, gameID );
+ Utils.postNotification( this, intent, title, body, (int)rowid );
}
// Runs in separate thread
@@ -669,11 +660,9 @@ public class SMSService extends Service {
@Override
public void onReceive(Context arg0, Intent arg1)
{
- DbgUtils.logf( "got MSG_DELIVERED" );
switch ( getResultCode() ) {
case Activity.RESULT_OK:
sendResult( MultiEvent.SMS_SEND_OK );
- DbgUtils.logf( "SUCCESS!!!" );
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
DbgUtils.showf( SMSService.this, "NO RADIO!!!" );
@@ -693,7 +682,6 @@ public class SMSService extends Service {
@Override
public void onReceive(Context arg0, Intent arg1)
{
- DbgUtils.logf( "got MSG_DELIVERED" );
if ( Activity.RESULT_OK == getResultCode() ) {
DbgUtils.logf( "SUCCESS!!!" );
} else {
@@ -714,7 +702,6 @@ public class SMSService extends Service {
public int transportSend( byte[] buf, final CommsAddrRec addr, int gameID )
{
int nSent = -1;
- DbgUtils.logf( "SMSMsgSink.transportSend()" );
if ( null != addr ) {
nSent = sendPacket( addr.sms_phone, gameID, buf );
} else {
@@ -757,7 +744,6 @@ public class SMSService extends Service {
public boolean isComplete()
{
boolean complete = m_msgs.length == m_haveCount;
- DbgUtils.logf( "isComplete(msg %d)=>%b", m_msgID, complete );
return complete;
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/StatusNotifier.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/StatusNotifier.java
deleted file mode 100644
index 6fa126c6e..000000000
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/StatusNotifier.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
-/*
- * Copyright 2012 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.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-
-public class StatusNotifier {
- private int m_id;
- private NotificationManager m_mgr;
- private Context m_context;
-
- public StatusNotifier( Context context, String msg, int id )
- {
- m_context = context;
- m_id = id;
-
- Notification notification =
- new Notification( R.drawable.icon48x48, msg,
- System.currentTimeMillis() );
- notification.flags = notification.flags |= Notification.FLAG_AUTO_CANCEL;
- PendingIntent pi = PendingIntent.getActivity( context, 0,
- new Intent(), 0 );
- notification.setLatestEventInfo( context, "", "", pi );
-
- m_mgr = (NotificationManager)
- context.getSystemService( Context.NOTIFICATION_SERVICE );
- m_mgr.notify( id, notification );
- }
-
- // Will likely be called from background thread
- public void close()
- {
- m_mgr.cancel( m_id );
- }
-
-}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/UpdateCheckReceiver.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/UpdateCheckReceiver.java
index a87f77537..932f699db 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/UpdateCheckReceiver.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/UpdateCheckReceiver.java
@@ -28,7 +28,9 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
+import android.os.AsyncTask;
import android.os.SystemClock;
+import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -153,68 +155,8 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
}
if ( 0 < params.length() ) {
- HttpPost post = makePost( context, "getUpdates" );
- String json = runPost( post, params );
- makeNotificationsIf( context, fromUI, json, pm, packageName, dals );
- }
- }
-
- private static void makeNotificationsIf( Context context, boolean fromUI,
- String jstr, PackageManager pm,
- String packageName,
- DictUtils.DictAndLoc[] dals )
- {
- boolean gotOne = false;
- try {
- JSONObject jobj = new JSONObject( jstr );
- if ( null != jobj ) {
- if ( jobj.has( k_APP ) ) {
- JSONObject app = jobj.getJSONObject( k_APP );
- if ( app.has( k_URL ) ) {
- String url = app.getString( k_URL );
- ApplicationInfo ai = pm.getApplicationInfo( packageName, 0);
- String label = pm.getApplicationLabel( ai ).toString();
- Intent intent =
- new Intent( Intent.ACTION_VIEW, Uri.parse(url) );
- String title =
- Utils.format( context, R.string.new_app_availf, label );
- String body = context.getString( R.string.new_app_avail );
- Utils.postNotification( context, intent, title, body,
- url.hashCode() );
- gotOne = true;
- }
- }
- if ( jobj.has( k_DICTS ) ) {
- JSONArray dicts = jobj.getJSONArray( k_DICTS );
- for ( int ii = 0; ii < dicts.length(); ++ii ) {
- JSONObject dict = dicts.getJSONObject( ii );
- if ( dict.has( k_URL ) && dict.has( k_INDEX ) ) {
- String url = dict.getString( k_URL );
- int index = dict.getInt( k_INDEX );
- DictUtils.DictAndLoc dal = dals[index];
- Intent intent =
- new Intent( context, DictsActivity.class );
- intent.putExtra( NEW_DICT_URL, url );
- intent.putExtra( NEW_DICT_LOC, dal.loc.ordinal() );
- String body =
- Utils.format( context, R.string.new_dict_availf,
- dal.name );
- Utils.postNotification( context, intent,
- R.string.new_dict_avail,
- body, url.hashCode() );
- gotOne = true;
- }
- }
- }
- }
- } catch ( org.json.JSONException jse ) {
- DbgUtils.loge( jse );
- } catch ( PackageManager.NameNotFoundException nnfe ) {
- DbgUtils.loge( nnfe );
- }
-
- if ( !gotOne && fromUI ) {
- Utils.showToast( context, R.string.checkupdates_none_found );
+ new UpdateQueryTask( context, params, fromUI, pm,
+ packageName, dals ).execute();
}
}
@@ -278,4 +220,121 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
false );
}
+ private static class UpdateQueryTask extends AsyncTask {
+ private Context m_context;
+ private JSONObject m_params;
+ private boolean m_fromUI;
+ private PackageManager m_pm;
+ private String m_packageName;
+ private DictUtils.DictAndLoc[] m_dals;
+
+ public UpdateQueryTask( Context context, JSONObject params,
+ boolean fromUI, PackageManager pm,
+ String packageName,
+ DictUtils.DictAndLoc[] dals )
+ {
+ m_context = context;
+ m_params = params;
+ m_fromUI = fromUI;
+ m_pm = pm;
+ m_packageName = packageName;
+ m_dals = dals;
+ }
+
+ @Override protected String doInBackground( Void... unused )
+ {
+ HttpPost post = makePost( m_context, "getUpdates" );
+ String json = runPost( post, m_params );
+ return json;
+ }
+
+ @Override protected void onPostExecute( String json )
+ {
+ if ( null != json ) {
+ makeNotificationsIf( json );
+ }
+ }
+
+ private void makeNotificationsIf( String jstr )
+ {
+ boolean gotOne = false;
+ try {
+ JSONObject jobj = new JSONObject( jstr );
+ if ( null != jobj ) {
+ if ( jobj.has( k_APP ) ) {
+ JSONObject app = jobj.getJSONObject( k_APP );
+ if ( app.has( k_URL ) ) {
+ ApplicationInfo ai =
+ m_pm.getApplicationInfo( m_packageName, 0);
+ String label = m_pm.getApplicationLabel( ai ).toString();
+
+ // If there's a download dir AND an installer
+ // app, handle this ourselves. Otherwise just
+ // launch the browser
+ boolean useBrowser;
+ File downloads = DictUtils.getDownloadDir( m_context );
+ if ( null == downloads ) {
+ useBrowser = true;
+ } else {
+ File tmp = new File( downloads,
+ "xx" + XWConstants.APK_EXTN );
+ useBrowser = !Utils.canInstall( m_context, tmp );
+ }
+
+ Intent intent;
+ String url = app.getString( k_URL );
+ if ( useBrowser ) {
+ intent = new Intent( Intent.ACTION_VIEW,
+ Uri.parse(url) );
+ } else {
+ intent = DictImportActivity
+ .makeAppDownloadIntent( m_context, url );
+ }
+
+ String title =
+ Utils.format( m_context, R.string.new_app_availf,
+ label );
+ String body =
+ m_context.getString( R.string.new_app_avail );
+ Utils.postNotification( m_context, intent, title,
+ body, url.hashCode() );
+ gotOne = true;
+ }
+ }
+ if ( jobj.has( k_DICTS ) ) {
+ JSONArray dicts = jobj.getJSONArray( k_DICTS );
+ for ( int ii = 0; ii < dicts.length(); ++ii ) {
+ JSONObject dict = dicts.getJSONObject( ii );
+ if ( dict.has( k_URL ) && dict.has( k_INDEX ) ) {
+ String url = dict.getString( k_URL );
+ int index = dict.getInt( k_INDEX );
+ DictUtils.DictAndLoc dal = m_dals[index];
+ Intent intent =
+ new Intent( m_context, DictsActivity.class );
+ intent.putExtra( NEW_DICT_URL, url );
+ intent.putExtra( NEW_DICT_LOC, dal.loc.ordinal() );
+ String body =
+ Utils.format( m_context,
+ R.string.new_dict_availf,
+ dal.name );
+ Utils.postNotification( m_context, intent,
+ R.string.new_dict_avail,
+ body, url.hashCode() );
+ gotOne = true;
+ }
+ }
+ }
+ }
+ } catch ( org.json.JSONException jse ) {
+ DbgUtils.loge( jse );
+ } catch ( PackageManager.NameNotFoundException nnfe ) {
+ DbgUtils.loge( nnfe );
+ }
+
+ if ( !gotOne && m_fromUI ) {
+ Utils.showToast( m_context, R.string.checkupdates_none_found );
+ }
+ }
+ }
+
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/Utils.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/Utils.java
index 6fd3d0afd..6d74998aa 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/Utils.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/Utils.java
@@ -32,19 +32,25 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.PhoneLookup;
import android.telephony.TelephonyManager;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
+import java.io.File;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Random;
import junit.framework.Assert;
@@ -60,6 +66,7 @@ public class Utils {
private static Boolean s_isFirstBootThisVersion = null;
private static Boolean s_deviceSupportSMS = null;
private static Boolean s_isFirstBootEver = null;
+ private static Integer s_appVersion = null;
private static HashMap s_phonesHash =
new HashMap();
private static int s_nextCode = 0; // keep PendingIntents unique
@@ -169,7 +176,8 @@ public class Utils {
}
public static void postNotification( Context context, Intent intent,
- String title, String body, int id )
+ String title, String body,
+ int id )
{
/* s_nextCode: per this link
http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager
@@ -339,6 +347,12 @@ public class Utils {
}
}
+ public static void setItemVisible( Menu menu, int id, boolean enabled )
+ {
+ MenuItem item = menu.findItem( id );
+ item.setVisible( enabled );
+ }
+
public static boolean hasSmallScreen( Context context )
{
if ( null == s_hasSmallScreen ) {
@@ -403,18 +417,49 @@ public class Utils {
return dict_url;
}
+ public static int getAppVersion( Context context )
+ {
+ if ( null == s_appVersion ) {
+ try {
+ int version = context.getPackageManager()
+ .getPackageInfo(context.getPackageName(), 0)
+ .versionCode;
+ s_appVersion = new Integer( version );
+ } catch ( Exception e ) {
+ DbgUtils.loge( e );
+ }
+ }
+ return null == s_appVersion? 0 : s_appVersion;
+ }
+
+ public static Intent makeInstallIntent( File file )
+ {
+ String withScheme = "file://" + file.getPath();
+ Uri uri = Uri.parse( withScheme );
+ Intent intent = new Intent( Intent.ACTION_VIEW );
+ intent.setDataAndType( uri, XWConstants.APK_TYPE );
+ intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
+ return intent;
+ }
+
+ // Return whether there's an app installed that can install
+ public static boolean canInstall( Context context, File path )
+ {
+ boolean result = false;
+ PackageManager pm = context.getPackageManager();
+ Intent intent = makeInstallIntent( path );
+ List doers =
+ pm.queryIntentActivities( intent,
+ PackageManager.MATCH_DEFAULT_ONLY );
+ result = 0 < doers.size();
+ return result;
+ }
+
private static void setFirstBootStatics( Context context )
{
- int thisVersion = 0;
+ int thisVersion = getAppVersion( context );
int prevVersion = 0;
- try {
- thisVersion = context.getPackageManager()
- .getPackageInfo(context.getPackageName(), 0)
- .versionCode;
- } catch ( Exception e ) {
- }
-
SharedPreferences prefs = null;
if ( 0 < thisVersion ) {
prefs = context.getSharedPreferences( HIDDEN_PREFS,
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWActivity.java
index b97d82ed5..71e8a922d 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWActivity.java
@@ -31,7 +31,7 @@ import android.widget.TextView;
import junit.framework.Assert;
public class XWActivity extends Activity
- implements DlgDelegate.DlgClickNotify, MultiService.BTEventListener {
+ implements DlgDelegate.DlgClickNotify, MultiService.MultiEventListener {
private DlgDelegate m_delegate;
@@ -48,7 +48,6 @@ public class XWActivity extends Activity
{
DbgUtils.logf( "%s.onStart(this=%H)", getClass().getName(), this );
super.onStart();
- DispatchNotify.SetRunning( this );
}
@Override
@@ -73,7 +72,6 @@ public class XWActivity extends Activity
protected void onStop()
{
DbgUtils.logf( "%s.onStop(this=%H)", getClass().getName(), this );
- DispatchNotify.ClearRunning( this );
super.onStop();
}
@@ -194,7 +192,7 @@ public class XWActivity extends Activity
Assert.fail();
}
- // BTService.BTEventListener interface
+ // BTService.MultiEventListener interface
public void eventOccurred( MultiService.MultiEvent event,
final Object ... args )
{
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWApp.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWApp.java
index 6c1623967..d3d02f534 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWApp.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWApp.java
@@ -28,11 +28,14 @@ import java.util.UUID;
import org.eehouse.android.xw4.jni.XwJNI;
public class XWApp extends Application {
- public static final boolean DEBUG_LOCKS = false;
public static final boolean BTSUPPORTED = false;
public static final boolean SMSSUPPORTED = true;
public static final boolean GCMSUPPORTED = true;
- public static final boolean DEBUG = false;
+ public static final boolean ATTACH_SUPPORTED = true;
+ public static final boolean REMATCH_SUPPORTED = false;
+ public static final boolean DEBUG = true;
+ public static final boolean DEBUG_LOCKS = false && DEBUG;
+ public static final boolean DEBUG_EXP_TIMERS = false && DEBUG;
public static final String SMS_PUBLIC_HEADER = "-XW4";
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWConstants.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWConstants.java
index 9dee0732b..456f8fff6 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWConstants.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWConstants.java
@@ -23,4 +23,7 @@ package org.eehouse.android.xw4;
public interface XWConstants {
public static final String GAME_EXTN = ".xwg";
public static final String DICT_EXTN = ".xwd";
+ public static final String APK_EXTN = ".apk";
+ public static final String APK_TYPE =
+ "application/vnd.android.package-archive";
}
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListActivity.java
index 397085e70..927b73f78 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListActivity.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListActivity.java
@@ -28,7 +28,7 @@ import android.os.Bundle;
import junit.framework.Assert;
public class XWListActivity extends ListActivity
- implements DlgDelegate.DlgClickNotify, MultiService.BTEventListener {
+ implements DlgDelegate.DlgClickNotify, MultiService.MultiEventListener {
private DlgDelegate m_delegate;
@@ -45,7 +45,6 @@ public class XWListActivity extends ListActivity
{
DbgUtils.logf( "%s.onStart(this=%H)", getClass().getName(), this );
super.onStart();
- DispatchNotify.SetRunning( this );
}
@Override
@@ -70,7 +69,6 @@ public class XWListActivity extends ListActivity
protected void onStop()
{
DbgUtils.logf( "%s.onStop(this=%H)", getClass().getName(), this );
- DispatchNotify.ClearRunning( this );
super.onStop();
}
@@ -195,7 +193,7 @@ public class XWListActivity extends ListActivity
m_delegate.launchLookup( words, lang, forceList );
}
- // BTService.BTEventListener interface
+ // MultiService.MultiEventListener interface
public void eventOccurred( MultiService.MultiEvent event,
final Object ... args )
{
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListAdapter.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListAdapter.java
index 069191e81..decc6fde6 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListAdapter.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListAdapter.java
@@ -41,11 +41,14 @@ public abstract class XWListAdapter implements ListAdapter {
public boolean areAllItemsEnabled() { return true; }
public boolean isEnabled( int position ) { return true; }
public int getCount() { return m_count; }
- public long getItemId(int position) { return position; }
- public int getItemViewType(int position) { return 0; }
+ public Object getItem( int position ) { return null; }
+ public long getItemId( int position ) { return position; }
+ public int getItemViewType( int position ) {
+ return ListAdapter.IGNORE_ITEM_VIEW_TYPE;
+ }
public int getViewTypeCount() { return 1; }
public boolean hasStableIds() { return true; }
public boolean isEmpty() { return getCount() == 0; }
- public void registerDataSetObserver(DataSetObserver observer) {}
- public void unregisterDataSetObserver(DataSetObserver observer) {}
+ public void registerDataSetObserver( DataSetObserver observer ) {}
+ public void unregisterDataSetObserver( DataSetObserver observer ) {}
}
\ No newline at end of file
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWPrefs.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWPrefs.java
index dac4ac1a0..b966a051e 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWPrefs.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWPrefs.java
@@ -24,6 +24,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
+import com.google.android.gcm.GCMRegistrar;
import java.util.ArrayList;
import java.util.ArrayList;
@@ -44,11 +45,6 @@ public class XWPrefs {
return getPrefsString( context, R.string.key_relay_host );
}
- public static String getDefaultRedirHost( Context context )
- {
- return getPrefsString( context, R.string.key_redir_host );
- }
-
public static int getDefaultRelayPort( Context context )
{
String val = getPrefsString( context, R.string.key_relay_port );
@@ -87,6 +83,11 @@ public class XWPrefs {
return getPrefsBoolean( context, R.string.key_ringer_zoom, false );
}
+ public static boolean getSquareTiles( Context context )
+ {
+ return getPrefsBoolean( context, R.string.key_square_tiles, false );
+ }
+
public static int getDefaultPlayerMinutes( Context context )
{
String value =
@@ -112,6 +113,24 @@ public class XWPrefs {
return result;
}
+ public static int getPrefsInt( Context context, int keyID, int defaultValue )
+ {
+ String key = context.getString( keyID );
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences( context );
+ return sp.getInt( key, defaultValue );
+ }
+
+ public static void setPrefsInt( Context context, int keyID, int newValue )
+ {
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences( context );
+ SharedPreferences.Editor editor = sp.edit();
+ String key = context.getString( keyID );
+ editor.putInt( key, newValue );
+ editor.commit();
+ }
+
public static boolean getPrefsBoolean( Context context, int keyID,
boolean defaultValue )
{
@@ -132,6 +151,25 @@ public class XWPrefs {
editor.commit();
}
+ public static long getPrefsLong( Context context, int keyID,
+ long defaultValue )
+ {
+ String key = context.getString( keyID );
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences( context );
+ return sp.getLong( key, defaultValue );
+ }
+
+ public static void setPrefsLong( Context context, int keyID, long newVal )
+ {
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences( context );
+ SharedPreferences.Editor editor = sp.edit();
+ String key = context.getString( keyID );
+ editor.putLong( key, newVal );
+ editor.commit();
+ }
+
public static void setClosedLangs( Context context, String[] langs )
{
setPrefsString( context, R.string.key_closed_langs,
@@ -186,17 +224,27 @@ public class XWPrefs {
public static void setGCMDevID( Context context, String devID )
{
- setPrefsString( context, R.string.key_gcm_regid, devID );
+ int curVers = Utils.getAppVersion( context );
+ setPrefsInt( context, R.string.key_gcmvers_regid, curVers );
+ clearPrefsKey( context, R.string.key_relay_regid );
}
public static String getGCMDevID( Context context )
{
- return getPrefsString( context, R.string.key_gcm_regid );
+ int curVers = Utils.getAppVersion( context );
+ int storedVers = getPrefsInt( context, R.string.key_gcmvers_regid, 0 );
+ String result;
+ if ( 0 != storedVers && storedVers < curVers ) {
+ result = ""; // Don't trust what registrar has
+ } else {
+ result = GCMRegistrar.getRegistrationId( context );
+ }
+ return result;
}
public static void clearGCMDevID( Context context )
{
- clearPrefsKey( context, R.string.key_gcm_regid );
+ clearRelayDevID( context );
}
public static String getRelayDevID( Context context )
@@ -213,6 +261,11 @@ public class XWPrefs {
setPrefsString( context, R.string.key_relay_regid, idRelay );
}
+ public static void clearRelayDevID( Context context )
+ {
+ clearPrefsKey( context, R.string.key_relay_regid );
+ }
+
public static boolean getHaveCheckedSMS( Context context )
{
return getPrefsBoolean( context, R.string.key_checked_sms, false );
@@ -241,6 +294,17 @@ public class XWPrefs {
return getPrefsBoolean( context, R.string.key_default_loc, true );
}
+ public static long getDefaultNewGameGroup( Context context )
+ {
+ return getPrefsLong( context, R.string.key_default_group,
+ DBUtils.ROWID_NOTFOUND );
+ }
+
+ public static void setDefaultNewGameGroup( Context context, long val )
+ {
+ setPrefsLong( context, R.string.key_default_group, val );
+ }
+
protected static String getPrefsString( Context context, int keyID )
{
String key = context.getString( keyID );
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/JNIThread.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/JNIThread.java
index 74128edab..39858aade 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/JNIThread.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/JNIThread.java
@@ -22,18 +22,20 @@
package org.eehouse.android.xw4.jni;
import android.content.Context;
-import java.lang.InterruptedException;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.Iterator;
-import android.os.Handler;
-import android.os.Message;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import java.lang.InterruptedException;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.concurrent.LinkedBlockingQueue;
import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.DbgUtils;
import org.eehouse.android.xw4.ConnStatusHandler;
import org.eehouse.android.xw4.BoardDims;
+import org.eehouse.android.xw4.GameLock;
import org.eehouse.android.xw4.GameUtils;
import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.Toolbar;
@@ -77,7 +79,7 @@ public class JNIThread extends Thread {
CMD_COUNTS_VALUES,
CMD_REMAINING,
CMD_RESEND,
- CMD_ACKANY,
+ // CMD_ACKANY,
CMD_HISTORY,
CMD_FINAL,
CMD_ENDGAME,
@@ -94,6 +96,7 @@ public class JNIThread extends Thread {
public static final int QUERY_ENDGAME = 4;
public static final int TOOLBAR_STATES = 5;
public static final int GOT_WORDS = 6;
+ public static final int GAME_OVER = 7;
public class GameStateInfo implements Cloneable {
public int visTileCount;
@@ -120,7 +123,8 @@ public class JNIThread extends Thread {
private boolean m_stopped = false;
private boolean m_saveOnStop = false;
private int m_jniGamePtr;
- private GameUtils.GameLock m_lock;
+ private byte[] m_gameAtStart;
+ private GameLock m_lock;
private Context m_context;
private CurGameInfo m_gi;
private Handler m_handler;
@@ -141,10 +145,12 @@ public class JNIThread extends Thread {
Object[] m_args;
}
- public JNIThread( int gamePtr, CurGameInfo gi, SyncedDraw drawer,
- GameUtils.GameLock lock, Context context, Handler handler )
+ public JNIThread( int gamePtr, byte[] gameAtStart, CurGameInfo gi,
+ SyncedDraw drawer, GameLock lock, Context context,
+ Handler handler )
{
m_jniGamePtr = gamePtr;
+ m_gameAtStart = gameAtStart;
m_gi = gi;
m_drawer = drawer;
m_lock = lock;
@@ -284,13 +290,17 @@ public class JNIThread extends Thread {
if ( null != m_newDict ) {
m_gi.dictName = m_newDict;
}
- GameSummary summary = new GameSummary( m_context, m_gi );
- XwJNI.game_summarize( m_jniGamePtr, summary );
byte[] state = XwJNI.game_saveToStream( m_jniGamePtr, m_gi );
- GameUtils.saveGame( m_context, state, m_lock, false );
- DBUtils.saveSummary( m_context, m_lock, summary );
- // There'd better be no way for saveGame above to fail!
- XwJNI.game_saveSucceeded( m_jniGamePtr );
+ if ( Arrays.equals( m_gameAtStart, state ) ) {
+ DbgUtils.logf( "no change in game; can skip saving" );
+ } else {
+ GameSummary summary = new GameSummary( m_context, m_gi );
+ XwJNI.game_summarize( m_jniGamePtr, summary );
+ DBUtils.saveGame( m_context, m_lock, state, false );
+ DBUtils.saveSummary( m_context, m_lock, summary );
+ // There'd better be no way for saveGame above to fail!
+ XwJNI.game_saveSucceeded( m_jniGamePtr );
+ }
}
@SuppressWarnings("fallthrough")
@@ -495,11 +505,12 @@ public class JNIThread extends Thread {
case CMD_RESEND:
XwJNI.comms_resendAll( m_jniGamePtr,
- ((Boolean)args[0]).booleanValue() );
- break;
- case CMD_ACKANY:
- XwJNI.comms_ackAny( m_jniGamePtr );
+ ((Boolean)args[0]).booleanValue(),
+ ((Boolean)args[1]).booleanValue() );
break;
+ // case CMD_ACKANY:
+ // XwJNI.comms_ackAny( m_jniGamePtr );
+ // break;
case CMD_HISTORY:
boolean gameOver = XwJNI.server_getGameIsOver( m_jniGamePtr );
@@ -523,8 +534,14 @@ public class JNIThread extends Thread {
case CMD_POST_OVER:
if ( XwJNI.server_getGameIsOver( m_jniGamePtr ) ) {
- sendForDialog( R.string.finalscores_title,
- XwJNI.server_writeFinalScores( m_jniGamePtr ) );
+ boolean auto = 0 < args.length &&
+ ((Boolean)args[0]).booleanValue();
+ int titleID = auto? R.string.summary_gameover
+ : R.string.finalscores_title;
+
+ String text = XwJNI.server_writeFinalScores( m_jniGamePtr );
+ Message.obtain( m_handler, GAME_OVER, titleID, 0, text )
+ .sendToTarget();
}
break;
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java
index 113cb8f86..5302f41f7 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxt.java
@@ -57,11 +57,12 @@ public interface UtilCtxt {
void setIsServer( boolean isServer );
// Possible values for typ[0], these must match enum in xwrelay.sh
+ public static final int ID_TYPE_NONE = 0;
public static final int ID_TYPE_RELAY = 1;
public static final int ID_TYPE_ANDROID_GCM = 3;
String getDevID( /*out*/ byte[] typ );
- void deviceRegistered( String idRelay );
+ void deviceRegistered( int devIDType, String idRelay );
void bonusSquareHeld( int bonus );
void playerScoreHeld( int player );
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
index e1410c915..b4ad0eb6a 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/UtilCtxtImpl.java
@@ -94,21 +94,37 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "setIsServer" );
}
- public String getDevID( /*out*/ byte[] typ )
+ public String getDevID( /*out*/ byte[] typa )
{
+ byte typ = UtilCtxt.ID_TYPE_NONE;
String result = XWPrefs.getRelayDevID( m_context );
if ( null != result ) {
- typ[0] = UtilCtxt.ID_TYPE_RELAY;
+ typ = UtilCtxt.ID_TYPE_RELAY;
} else {
result = XWPrefs.getGCMDevID( m_context );
- typ[0] = UtilCtxt.ID_TYPE_ANDROID_GCM;
+ if ( result.equals("") ) {
+ result = null;
+ } else {
+ typ = UtilCtxt.ID_TYPE_ANDROID_GCM;
+ }
}
+ typa[0] = typ;
return result;
}
- public void deviceRegistered( String idRelay )
+ public void deviceRegistered( int devIDType, String idRelay )
{
- XWPrefs.setRelayDevID( m_context, idRelay );
+ switch ( devIDType ) {
+ case UtilCtxt.ID_TYPE_RELAY:
+ XWPrefs.setRelayDevID( m_context, idRelay );
+ break;
+ case UtilCtxt.ID_TYPE_NONE:
+ XWPrefs.clearRelayDevID( m_context );
+ break;
+ default:
+ Assert.fail();
+ break;
+ }
}
public void bonusSquareHeld( int bonus )
diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/XwJNI.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/XwJNI.java
index 43809fada..0d869ab06 100644
--- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/XwJNI.java
+++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/jni/XwJNI.java
@@ -238,7 +238,8 @@ public class XwJNI {
public static native void comms_getAddr( int gamePtr, CommsAddrRec addr );
public static native CommsAddrRec[] comms_getAddrs( int gamePtr );
public static native void comms_setAddr( int gamePtr, CommsAddrRec addr );
- public static native void comms_resendAll( int gamePtr, boolean andAck );
+ public static native void comms_resendAll( int gamePtr, boolean force,
+ boolean andAck );
public static native void comms_ackAny( int gamePtr );
public static native void comms_transportFailed( int gamePtr );
public static native boolean comms_isConnected( int gamePtr );
diff --git a/xwords4/android/scripts/and_index.php b/xwords4/android/scripts/and_index.php
new file mode 100644
index 000000000..7d0a53cc1
--- /dev/null
+++ b/xwords4/android/scripts/and_index.php
@@ -0,0 +1,112 @@
+
+
+
+
+ Crosswords Invite redirect
+
+
+
+
+
+EOF;
+}
+
+function printTail() {
+print <<
+
+EOF;
+}
+
+function printNonAndroid($agent) {
+ $subject = "Android device not identified";
+
+ $body = htmlentities("My browser is running on an android device but"
+ . " says its user agent is: \"$agent\"."
+ . " Please fix your website to recognize"
+ . " this as an Android browser.");
+ print <<
+
This page is meant to be viewed on an Android device.
+
+
(If you are viewing this on an Android device,
+ you've found a bug! Please email me
+ (and be sure to leave the user agent string in the email body.)
+
You'll have come here after clicking a link in an email or
+ text inviting you to a Crosswords game. But you should not be seeing
+ this page.
+
+
If you got this page on your device, it means either
+
+
The copy of Crosswords you have is NOT beta 56 or newer (dating from about Dec. 1, 2012).
+
OR
+
that your copy of Crosswords is new enough BUT that
+ when you clicked on the link and were asked to choose between a
+ browser and Crosswords you chose the browser.
+
+
+
In the first case, install the latest Crosswords,
+either via
+the Google Play store or
+(sideloading) via
+Sourceforge.net. After the install is finished go back to the
+invite email (or text) and tap the link again.
+
+
In the second case, hit your browser's back button, click the
+link in your invite email (or text) again, and this time let
+Crosswords handle it.
+
+
(If you get tired of having to having to make that choice, Android
+will allow you to make Crosswords the default. If you do that
+Crosswords will be given control of all URLs that start with
+"http://eehouse.org/and/" -- not all URLs of any type.)
+
+
Have fun. And as always, let
+me know if you have problems or suggestions.
This page is meant to be viewed (briefly) on your Android device after which Crosswords should launch.
- If this fails it's probably because you don't have a new enough version of Crosswords installed.
+
It appears you're running on a Kindle Fire, whose non-standard (from
+an Android perspective) OS doesn't support the custom schemes on which
+Crosswords invitations depend. If you want to accept this invitation
+you'll need to do it the manual way:
+
+
+
Open Crosswords, and navigate to the main Games List screen
+
Choose "Add game", either from the menu or the button at the bottom.
+
Under "New Networked game", choose "Configure first".
+
$langText
+
As the room name, enter "$room".
+
Make sure the total number of players shown is $np and that only one of them is not an "Off-device player".
+
Now tap the "Play game" button at the bottom (above the keyboard). Your new game should open and connect.
+
+
I'm sorry this is so complicated. I'm trying to find a
+workaround for this limitation in the Kindle Fire's operating system
+but for now this is all I can offer.
+
+
(Just in case Amazon's fixed the
+problem, here is the link that should open
+your new game.)
+
+
+EOF;
+} else {
+$subject = "Android device not identified";
+
+$body = htmlentities("My browser is running on an android device but"
+. " says its user agent is: \"$agent\". Please fix your script to recognize"
+. " this as an Android browser.");
+
+print <<
+
+
+Crosswords Invite redirect
+
+
+
+
+
+
This page is meant to be viewed on a browser on your Android
+ device. Please open the email that sent you here on that device and
+ revisit this link to complete the invitation process.
+
+
+
(If you are viewing this on an Android device, you've
+ found a bug! Please email me (and be
+ sure to leave the user agent string in the email body.)
+
+
+
+
+
+EOF;
+}
+
?>
diff --git a/xwords4/android/scripts/xw4mobile.css b/xwords4/android/scripts/xw4mobile.css
index a116fc92b..d628a7a8e 100644
--- a/xwords4/android/scripts/xw4mobile.css
+++ b/xwords4/android/scripts/xw4mobile.css
@@ -1,2 +1,3 @@
-body { font-size: 2em; }
-table { font-size: 2em; }
+body { font-size: 1.5em; }
+table { font-size: 1.5em; }
+.center { text-align: center; }
diff --git a/xwords4/common/comms.c b/xwords4/common/comms.c
index 498e38c04..4e7bcb387 100644
--- a/xwords4/common/comms.c
+++ b/xwords4/common/comms.c
@@ -1,6 +1,6 @@
/* -*- compile-command: "cd ../linux && make MEMDEBUG=TRUE -j3"; -*- */
/*
- * Copyright 2001-2011 by Eric House (xwords@eehouse.org). All rights
+ * Copyright 2001 - 2012 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@@ -55,7 +55,9 @@ typedef struct MsgQueueElem {
XP_U8* msg;
XP_U16 len;
XP_PlayerAddr channelNo;
+#ifdef DEBUG
XP_U16 sendCount; /* how many times sent? */
+#endif
MsgID msgID; /* saved for ease of deletion */
#ifdef COMMS_CHECKSUM
gchar* checksum;
@@ -108,6 +110,9 @@ struct CommsCtxt {
XP_U16 queueLen;
XP_U16 channelSeed; /* tries to be unique per device to aid
dupe elimination at start */
+ XP_U32 nextResend;
+ XP_U16 resendBackoff;
+
#ifdef COMMS_HEARTBEAT
XP_Bool doHeartbeat;
XP_U32 lastMsgRcvdTime;
@@ -572,6 +577,10 @@ comms_makeFromStream( MPFORMAL XWStreamCtxt* stream, XW_UtilCtxt* util,
comms->channelSeed = stream_getU16( stream );
XP_LOGF( "%s: loaded seed: %.4X", __func__, comms->channelSeed );
}
+ if ( STREAM_VERS_COMMSBACKOFF <= version ) {
+ comms->resendBackoff = stream_getU16( stream );
+ comms->nextResend = stream_getU32( stream );
+ }
if ( addr.conType == COMMS_CONN_RELAY ) {
comms->r.myHostID = stream_getU8( stream );
stringFromStreamHere( stream, comms->r.connName,
@@ -607,7 +616,7 @@ comms_makeFromStream( MPFORMAL XWStreamCtxt* stream, XW_UtilCtxt* util,
msg->channelNo = stream_getU16( stream );
msg->msgID = stream_getU32( stream );
-#ifdef COMMS_HEARTBEAT
+#ifdef DEBUG
msg->sendCount = 0;
#endif
msg->len = stream_getU16( stream );
@@ -672,7 +681,7 @@ sendConnect( CommsCtxt* comms, XP_Bool breakExisting )
case COMMS_CONN_IP_DIRECT:
/* This will only work on host side when there's a single guest! */
(void)send_via_bt_or_ip( comms, BTIPMSG_RESET, CHANNEL_NONE, NULL, 0 );
- (void)comms_resendAll( comms );
+ (void)comms_resendAll( comms, XP_FALSE );
break;
#endif
default:
@@ -743,6 +752,8 @@ comms_writeToStream( CommsCtxt* comms, XWStreamCtxt* stream,
stream_putU32( stream, comms->connID );
stream_putU16( stream, comms->nextChannelNo );
stream_putU16( stream, comms->channelSeed );
+ stream_putU16( stream, comms->resendBackoff );
+ stream_putU32( stream, comms->nextResend );
if ( comms->addr.conType == COMMS_CONN_RELAY ) {
stream_putU8( stream, comms->r.myHostID );
stringToStream( stream, comms->r.connName );
@@ -779,15 +790,25 @@ comms_writeToStream( CommsCtxt* comms, XWStreamCtxt* stream,
comms->lastSaveToken = saveToken;
} /* comms_writeToStream */
+static void
+resetBackoff( CommsCtxt* comms )
+{
+ XP_LOGF( "%s: resetting backoff", __func__ );
+ comms->resendBackoff = 0;
+ comms->nextResend = 0;
+}
+
void
comms_saveSucceeded( CommsCtxt* comms, XP_U16 saveToken )
{
XP_LOGF( "%s(saveToken=%d)", __func__, saveToken );
XP_ASSERT( !!comms );
if ( saveToken == comms->lastSaveToken ) {
- XP_LOGF( "%s: lastSave matches", __func__ );
AddressRecord* rec;
for ( rec = comms->recs; !!rec; rec = rec->next ) {
+ XP_LOGF( "%s: lastSave matches; updating lastMsgSaved (%ld) to "
+ "lastMsgRcd (%ld)", __func__, rec->lastMsgSaved,
+ rec->lastMsgRcd );
rec->lastMsgSaved = rec->lastMsgRcd;
}
#ifdef XWFEATURE_COMMSACK
@@ -942,7 +963,7 @@ makeElemWithID( CommsCtxt* comms, MsgID msgID, AddressRecord* rec,
sizeof( *newMsgElem ) );
newMsgElem->channelNo = channelNo;
newMsgElem->msgID = msgID;
-#ifdef COMMS_HEARTBEAT
+#ifdef DEBUG
newMsgElem->sendCount = 0;
#endif
@@ -996,24 +1017,28 @@ comms_getChannelSeed( CommsCtxt* comms )
XP_S16
comms_send( CommsCtxt* comms, XWStreamCtxt* stream )
{
- XP_PlayerAddr channelNo = stream_getAddress( stream );
- XP_LOGF( "%s: channelNo=%x", __func__, channelNo );
- AddressRecord* rec = getRecordFor( comms, NULL, channelNo, XP_FALSE );
- MsgID msgID = (!!rec)? ++rec->nextMsgID : 0;
- MsgQueueElem* elem;
XP_S16 result = -1;
+ if ( 0 == stream_getSize(stream) ) {
+ XP_LOGF( "%s: dropping 0-len message", __func__ );
+ } else {
+ XP_PlayerAddr channelNo = stream_getAddress( stream );
+ XP_LOGF( "%s: channelNo=%x", __func__, channelNo );
+ AddressRecord* rec = getRecordFor( comms, NULL, channelNo, XP_FALSE );
+ MsgID msgID = (!!rec)? ++rec->nextMsgID : 0;
+ MsgQueueElem* elem;
- if ( 0 == channelNo ) {
- channelNo = comms_getChannelSeed(comms) & ~CHANNEL_MASK;
- }
+ if ( 0 == channelNo ) {
+ channelNo = comms_getChannelSeed(comms) & ~CHANNEL_MASK;
+ }
- XP_DEBUGF( "%s: assigning msgID=" XP_LD " on chnl %x", __func__,
- msgID, channelNo );
+ XP_DEBUGF( "%s: assigning msgID=" XP_LD " on chnl %x", __func__,
+ msgID, channelNo );
- elem = makeElemWithID( comms, msgID, rec, channelNo, stream );
- if ( NULL != elem ) {
- addToQueue( comms, elem );
- result = sendMsg( comms, elem );
+ elem = makeElemWithID( comms, msgID, rec, channelNo, stream );
+ if ( NULL != elem ) {
+ addToQueue( comms, elem );
+ result = sendMsg( comms, elem );
+ }
}
return result;
} /* comms_send */
@@ -1037,9 +1062,10 @@ addToQueue( CommsCtxt* comms, MsgQueueElem* newMsgElem )
XP_ASSERT( comms->queueLen > 0 );
}
++comms->queueLen;
- XP_LOGF( "%s: queueLen now %d after channelNo: %d; msgID: " XP_LD,
- __func__, comms->queueLen,
- newMsgElem->channelNo & CHANNEL_MASK, newMsgElem->msgID );
+ XP_LOGF( "%s: queueLen now %d after channelNo: %d; msgID: " XP_LD
+ "; len: %d", __func__, comms->queueLen,
+ newMsgElem->channelNo & CHANNEL_MASK, newMsgElem->msgID,
+ newMsgElem->len );
} /* addToQueue */
#ifdef DEBUG
@@ -1207,8 +1233,11 @@ sendMsg( CommsCtxt* comms, MsgQueueElem* elem )
}
if ( result == elem->len ) {
+#ifdef DEBUG
++elem->sendCount;
- XP_LOGF( "%s: elem's sendCount now %d", __func__, elem->sendCount );
+#endif
+ XP_LOGF( "%s: elem's sendCount since load: %d", __func__,
+ elem->sendCount );
}
XP_LOGF( "%s(channelNo=%d;msgID=" XP_LD ")=>%d", __func__,
@@ -1224,17 +1253,32 @@ send_ack( CommsCtxt* comms )
}
XP_Bool
-comms_resendAll( CommsCtxt* comms )
+comms_resendAll( CommsCtxt* comms, XP_Bool force )
{
XP_Bool success = XP_TRUE;
- MsgQueueElem* msg;
-
XP_ASSERT( !!comms );
- for ( msg = comms->msgQueueHead; !!msg; msg = msg->next ) {
- if ( 0 > sendMsg( comms, msg ) ) {
- success = XP_FALSE;
- break;
+ XP_U32 now = util_getCurSeconds( comms->util );
+ if ( !force && (now < comms->nextResend) ) {
+ XP_LOGF( "%s: aborting: %ld seconds left in backoff", __func__,
+ comms->nextResend - now );
+ success = XP_FALSE;
+
+ } else if ( !!comms->msgQueueHead ) {
+ MsgQueueElem* msg;
+
+ for ( msg = comms->msgQueueHead; !!msg; msg = msg->next ) {
+ if ( 0 > sendMsg( comms, msg ) ) {
+ success = XP_FALSE;
+ break;
+ }
+ }
+
+ /* Now set resend values */
+ if ( success && !force ) {
+ comms->resendBackoff = 2 * (1 + comms->resendBackoff);
+ XP_LOGF( "%s: backoff now %d", __func__, comms->resendBackoff );
+ comms->nextResend = now + comms->resendBackoff;
}
}
return success;
@@ -1244,14 +1288,25 @@ comms_resendAll( CommsCtxt* comms )
void
comms_ackAny( CommsCtxt* comms )
{
+#ifdef DEBUG
+ XP_Bool noneSent = XP_TRUE;
+#endif
AddressRecord* rec;
for ( rec = comms->recs; !!rec; rec = rec->next ) {
if ( rec->lastMsgAckd < rec->lastMsgRcd ) {
- XP_LOGF( "%s: %ld < %ld: rec needs ack", __func__,
- rec->lastMsgAckd, rec->lastMsgRcd );
+#ifdef DEBUG
+ noneSent = XP_FALSE;
+#endif
+ XP_LOGF( "%s: channel %x; %ld < %ld: rec needs ack", __func__,
+ rec->channelNo, rec->lastMsgAckd, rec->lastMsgRcd );
sendEmptyMsg( comms, rec );
}
}
+#ifdef DEBUG
+ if ( noneSent ) {
+ XP_LOGF( "%s: nothing to send", __func__ );
+ }
+#endif
}
#endif
@@ -1331,12 +1386,14 @@ got_connect_cmd( CommsCtxt* comms, XWStreamCtxt* stream,
#endif
#ifdef XWFEATURE_DEVID
- if ( !reconnected ) {
- XP_UCHAR devID[MAX_DEVID_LEN + 1];
+ DevIDType typ = stream_getU8( stream );
+ XP_UCHAR devID[MAX_DEVID_LEN + 1] = {0};
+ if ( ID_TYPE_NONE != typ ) {
stringFromStreamHere( stream, devID, sizeof(devID) );
- if ( devID[0] != '\0' ) {
- util_deviceRegistered( comms->util, devID );
- }
+ }
+ if ( ID_TYPE_NONE == typ /* error case */
+ || '\0' != devID[0] ) /* new info case */ {
+ util_deviceRegistered( comms->util, typ, devID );
}
#endif
@@ -1366,7 +1423,7 @@ relayPreProcess( CommsCtxt* comms, XWStreamCtxt* stream, XWHostID* senderID )
break;
case XWRELAY_RECONNECT_RESP:
got_connect_cmd( comms, stream, XP_TRUE );
- comms_resendAll( comms );
+ comms_resendAll( comms, XP_FALSE );
break;
case XWRELAY_ALLHERE:
@@ -1404,7 +1461,7 @@ relayPreProcess( CommsCtxt* comms, XWStreamCtxt* stream, XWHostID* senderID )
on RECONNECTED, so removing the test for now to fix recon
problems on android. */
/* if ( COMMS_RELAYSTATE_RECONNECTED != comms->r.relayState ) { */
- comms_resendAll( comms );
+ comms_resendAll( comms, XP_FALSE );
/* } */
if ( XWRELAY_ALLHERE == cmd ) { /* initial connect? */
(*comms->procs.rconnd)( comms->procs.closure,
@@ -1513,7 +1570,7 @@ btIpPreProcess( CommsCtxt* comms, XWStreamCtxt* stream )
if ( consumed ) {
/* This is all there is so far */
if ( typ == BTIPMSG_RESET ) {
- (void)comms_resendAll( comms );
+ (void)comms_resendAll( comms, XP_FALSE );
} else if ( typ == BTIPMSG_HB ) {
/* noteHBReceived( comms, addr ); */
} else {
@@ -1681,6 +1738,7 @@ validateInitialMessage( CommsCtxt* comms,
rec = getRecordFor( comms, addr, *channelNo, XP_TRUE );
if ( !!rec ) {
/* reject: we've already seen init message on channel */
+ XP_LOGF( "%s: rejecting duplicate INIT message", __func__ );
rec = NULL;
} else {
if ( comms->isServer ) {
@@ -1779,6 +1837,7 @@ comms_checkIncomingStream( CommsCtxt* comms, XWStreamCtxt* stream,
comms->lastSaveToken = 0; /* lastMsgRcd no longer valid */
stream_setAddress( stream, channelNo );
messageValid = payloadSize > 0;
+ resetBackoff( comms );
}
} else {
XP_LOGF( "%s: message too small", __func__ );
@@ -1843,7 +1902,7 @@ sendEmptyMsg( CommsCtxt* comms, AddressRecord* rec )
0 /*rec? rec->lastMsgRcd : 0*/,
rec,
rec? rec->channelNo : 0, NULL );
- sendMsg( comms, elem );
+ (void)sendMsg( comms, elem );
freeElem( comms, elem );
} /* sendEmptyMsg */
#endif
@@ -2157,6 +2216,7 @@ msg_to_stream( CommsCtxt* comms, XWRELAY_Cmd cmd, XWHostID destID,
stream_putU16( stream, comms_getChannelSeed(comms) );
stream_putU8( stream, comms->util->gameInfo->dictLang );
stringToStream( stream, comms->r.connName );
+ putDevID( comms, stream );
set_relay_state( comms, COMMS_RELAYSTATE_CONNECT_PENDING );
break;
@@ -2240,7 +2300,7 @@ sendNoConn( CommsCtxt* comms, const MsgQueueElem* elem, XWHostID destID )
}
}
- LOG_RETURNF( "%d", success );
+ LOG_RETURNF( "%s", success?"TRUE":"FALSE" );
return success;
}
diff --git a/xwords4/common/comms.h b/xwords4/common/comms.h
index 7907be490..04234c995 100644
--- a/xwords4/common/comms.h
+++ b/xwords4/common/comms.h
@@ -203,7 +203,7 @@ void comms_writeToStream( CommsCtxt* comms, XWStreamCtxt* stream,
void comms_saveSucceeded( CommsCtxt* comms, XP_U16 saveToken );
XP_S16 comms_send( CommsCtxt* comms, XWStreamCtxt* stream );
-XP_Bool comms_resendAll( CommsCtxt* comms );
+XP_Bool comms_resendAll( CommsCtxt* comms, XP_Bool force );
XP_U16 comms_getChannelSeed( CommsCtxt* comms );
#ifdef XWFEATURE_COMMSACK
diff --git a/xwords4/common/comtypes.h b/xwords4/common/comtypes.h
index b806fe124..805694f1e 100644
--- a/xwords4/common/comtypes.h
+++ b/xwords4/common/comtypes.h
@@ -47,6 +47,7 @@
#endif
#define MAX_COLS MAX_ROWS
+#define STREAM_VERS_COMMSBACKOFF 0x16
#define STREAM_VERS_DICTNAME 0x15
#ifdef HASH_STREAM
# define STREAM_VERS_HASHSTREAM 0x14
@@ -82,7 +83,7 @@
#define STREAM_VERS_41B4 0x02
#define STREAM_VERS_405 0x01
-#define CUR_STREAM_VERS STREAM_VERS_DICTNAME
+#define CUR_STREAM_VERS STREAM_VERS_COMMSBACKOFF
typedef struct XP_Rect {
XP_S16 left;
diff --git a/xwords4/common/game.c b/xwords4/common/game.c
index 6507ba204..ae73da301 100644
--- a/xwords4/common/game.c
+++ b/xwords4/common/game.c
@@ -93,10 +93,12 @@ game_makeNewGame( MPFORMAL XWGame* game, CurGameInfo* gi,
#endif
)
{
- XP_U16 nPlayersHere, nPlayersTotal;
-
- assertUtilOK( util );
+#ifndef XWFEATURE_STANDALONE_ONLY
+ XP_U16 nPlayersHere = 0;
+ XP_U16 nPlayersTotal = 0;
checkServerRole( gi, &nPlayersHere, &nPlayersTotal );
+#endif
+ assertUtilOK( util );
gi->gameID = makeGameID( util );
@@ -137,15 +139,17 @@ game_reset( MPFORMAL XWGame* game, CurGameInfo* gi,
CommonPrefs* cp, const TransportProcs* procs )
{
XP_U16 ii;
- XP_U16 nPlayersHere, nPlayersTotal;
XP_ASSERT( !!game->model );
XP_ASSERT( !!gi );
- checkServerRole( gi, &nPlayersHere, &nPlayersTotal );
gi->gameID = makeGameID( util );
#ifndef XWFEATURE_STANDALONE_ONLY
+ XP_U16 nPlayersHere = 0;
+ XP_U16 nPlayersTotal = 0;
+ checkServerRole( gi, &nPlayersHere, &nPlayersTotal );
+
if ( !!game->comms ) {
if ( gi->serverRole == SERVER_STANDALONE ) {
comms_destroy( game->comms );
@@ -473,6 +477,7 @@ gi_readFromStream( MPFORMAL XWStreamCtxt* stream, CurGameInfo* gi )
gi->nPlayers = (XP_U8)stream_getBits( stream, NPLAYERS_NBITS );
gi->boardSize = (XP_U8)stream_getBits( stream, nColsNBits );
gi->serverRole = (DeviceRole)stream_getBits( stream, 2 );
+ XP_LOGF( "%s: read role of %d", __func__, gi->serverRole );
gi->hintsNotAllowed = stream_getBits( stream, 1 );
if ( strVersion < STREAM_VERS_ROBOTIQ ) {
(void)stream_getBits( stream, 2 );
diff --git a/xwords4/common/movestak.c b/xwords4/common/movestak.c
index 140da1fb4..984d43795 100644
--- a/xwords4/common/movestak.c
+++ b/xwords4/common/movestak.c
@@ -147,7 +147,7 @@ stack_getHash( const StackCtxt* stack )
stream_copyBits( stack->data, 0, stack->top, buf, &len );
// LOG_HEX( buf, len, __func__ );
hash = finishHash( augmentHash( 0L, buf, len ) );
- LOG_RETURNF( "%.8X", (unsigned int)hash );
+ // LOG_RETURNF( "%.8X", (unsigned int)hash );
return hash;
} /* stack_getHash */
#endif
diff --git a/xwords4/common/server.c b/xwords4/common/server.c
index 95e4c0759..aebb41ab2 100644
--- a/xwords4/common/server.c
+++ b/xwords4/common/server.c
@@ -686,7 +686,7 @@ handleRegistrationMsg( ServerCtxt* server, XWStreamCtxt* stream )
{
XP_Bool success = XP_TRUE;
XP_U16 playersInMsg;
- XP_S8 clientIndex;
+ XP_S8 clientIndex = 0; /* quiet compiler */
XP_U16 ii = 0;
LOG_FUNC();
diff --git a/xwords4/common/util.h b/xwords4/common/util.h
index b114efdc6..29c64fd3d 100644
--- a/xwords4/common/util.h
+++ b/xwords4/common/util.h
@@ -154,7 +154,8 @@ typedef struct UtilVtable {
XP_U32 (*m_util_getCurSeconds)( XW_UtilCtxt* uc );
#ifdef XWFEATURE_DEVID
const XP_UCHAR* (*m_util_getDevID)( XW_UtilCtxt* uc, DevIDType* typ );
- void (*m_util_deviceRegistered)( XW_UtilCtxt* uc, const XP_UCHAR* idRelay );
+ void (*m_util_deviceRegistered)( XW_UtilCtxt* uc, DevIDType typ,
+ const XP_UCHAR* idRelay );
#endif
DictionaryCtxt* (*m_util_makeEmptyDict)( XW_UtilCtxt* uc );
@@ -284,10 +285,10 @@ struct XW_UtilCtxt {
(uc)->vtable->m_util_getCurSeconds((uc))
#ifdef XWFEATURE_DEVID
-# define util_getDevID( uc, t ) \
+# define util_getDevID( uc, t ) \
(uc)->vtable->m_util_getDevID((uc),(t))
-# define util_deviceRegistered( uc, id ) \
- (uc)->vtable->m_util_deviceRegistered( (uc), (id) )
+# define util_deviceRegistered( uc, typ, id ) \
+ (uc)->vtable->m_util_deviceRegistered( (uc), (typ), (id) )
#endif
#define util_makeEmptyDict( uc ) \
diff --git a/xwords4/linux/Makefile b/xwords4/linux/Makefile
index fe0af91a2..05543eeee 100644
--- a/xwords4/linux/Makefile
+++ b/xwords4/linux/Makefile
@@ -111,6 +111,7 @@ DEFINES += -DXWFEATURE_HILITECELL
# allow change dict inside running game
DEFINES += -DXWFEATURE_CHANGEDICT
DEFINES += -DXWFEATURE_DEVID
+DEFINES += -DXWFEATURE_COMMSACK
# MAX_ROWS controls STREAM_VERS_BIGBOARD and with it move hashing
DEFINES += -DMAX_ROWS=32
diff --git a/xwords4/linux/cursesmain.c b/xwords4/linux/cursesmain.c
index 42529981c..bd442f884 100644
--- a/xwords4/linux/cursesmain.c
+++ b/xwords4/linux/cursesmain.c
@@ -474,6 +474,7 @@ onetime_idle( gpointer data )
if ( !!globals->cGlobals.game.board ) {
board_draw( globals->cGlobals.game.board );
}
+ saveGame( &globals->cGlobals );
}
return FALSE;
}
@@ -579,7 +580,7 @@ static XP_Bool
handleResend( CursesAppGlobals* globals )
{
if ( !!globals->cGlobals.game.comms ) {
- comms_resendAll( globals->cGlobals.game.comms );
+ comms_resendAll( globals->cGlobals.game.comms, XP_TRUE );
}
return XP_TRUE;
}
@@ -1219,7 +1220,7 @@ static XP_Bool
blocking_gotEvent( CursesAppGlobals* globals, int* ch )
{
XP_Bool result = XP_FALSE;
- int numEvents;
+ int numEvents, ii;
short fdIndex;
XP_Bool redraw = XP_FALSE;
@@ -1334,12 +1335,15 @@ blocking_gotEvent( CursesAppGlobals* globals, int* ch )
}
}
- redraw = server_do( globals->cGlobals.game.server, NULL ) || redraw;
+ for ( ii = 0; ii < 5; ++ii ) {
+ redraw = server_do( globals->cGlobals.game.server, NULL ) || redraw;
+ }
if ( redraw ) {
/* messages change a lot */
board_invalAll( globals->cGlobals.game.board );
board_draw( globals->cGlobals.game.board );
}
+ saveGame( globals->cGlobals );
}
return result;
} /* blocking_gotEvent */
@@ -1486,25 +1490,15 @@ curses_util_remSelected( XW_UtilCtxt* uc )
}
#ifndef XWFEATURE_STANDALONE_ONLY
-static void
-cursesSendOnClose( XWStreamCtxt* stream, void* closure )
-{
- CursesAppGlobals* globals = (CursesAppGlobals*)closure;
-
- XP_LOGF( "cursesSendOnClose called" );
- (void)comms_send( globals->cGlobals.game.comms, stream );
-} /* cursesSendOnClose */
-
static XWStreamCtxt*
curses_util_makeStreamFromAddr(XW_UtilCtxt* uc, XP_PlayerAddr channelNo )
{
CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure;
LaunchParams* params = globals->cGlobals.params;
- XWStreamCtxt* stream = mem_stream_make( MPPARM(uc->mpool)
- params->vtMgr,
- uc->closure, channelNo,
- cursesSendOnClose );
+ XWStreamCtxt* stream = mem_stream_make( MPPARM(uc->mpool) params->vtMgr,
+ &globals->cGlobals, channelNo,
+ sendOnClose );
return stream;
} /* curses_util_makeStreamFromAddr */
#endif
@@ -1544,17 +1538,6 @@ setupCursesUtilCallbacks( CursesAppGlobals* globals, XW_UtilCtxt* util )
util->closure = globals;
} /* setupCursesUtilCallbacks */
-#ifndef XWFEATURE_STANDALONE_ONLY
-static void
-sendOnClose( XWStreamCtxt* stream, void* closure )
-{
- CursesAppGlobals* globals = closure;
- XP_LOGF( "curses sendOnClose called" );
- XP_ASSERT( !!globals->cGlobals.game.comms );
- comms_send( globals->cGlobals.game.comms, stream );
-} /* sendOnClose */
-#endif
-
static CursesMenuHandler
getHandlerForKey( const MenuList* list, char ch )
{
@@ -1871,7 +1854,7 @@ cursesmain( XP_Bool isServer, LaunchParams* params )
server_initClientConnection( g_globals.cGlobals.game.server,
mem_stream_make( MEMPOOL
params->vtMgr,
- &g_globals,
+ &g_globals.cGlobals,
(XP_PlayerAddr)0,
sendOnClose ) );
} else {
diff --git a/xwords4/linux/gtkmain.c b/xwords4/linux/gtkmain.c
index 0567410f7..af2ee6cbc 100644
--- a/xwords4/linux/gtkmain.c
+++ b/xwords4/linux/gtkmain.c
@@ -65,9 +65,6 @@
#include "filestream.h"
/* static guint gtkSetupClientSocket( GtkAppGlobals* globals, int sock ); */
-#ifndef XWFEATURE_STANDALONE_ONLY
-static void sendOnCloseGTK( XWStreamCtxt* stream, void* closure );
-#endif
static void setCtrlsForTray( GtkAppGlobals* globals );
static void new_game( GtkWidget* widget, GtkAppGlobals* globals );
static void new_game_impl( GtkAppGlobals* globals, XP_Bool fireConnDlg );
@@ -508,8 +505,8 @@ createOrLoadObjects( GtkAppGlobals* globals )
#ifndef XWFEATURE_STANDALONE_ONLY
} else if ( !isServer ) {
XWStreamCtxt* stream =
- mem_stream_make( MEMPOOL params->vtMgr, globals, CHANNEL_NONE,
- sendOnCloseGTK );
+ mem_stream_make( MEMPOOL params->vtMgr, &globals->cGlobals, CHANNEL_NONE,
+ sendOnClose );
server_initClientConnection( globals->cGlobals.game.server,
stream );
#endif
@@ -814,11 +811,9 @@ new_game_impl( GtkAppGlobals* globals, XP_Bool fireConnDlg )
if ( isClient ) {
XWStreamCtxt* stream =
- mem_stream_make( MEMPOOL
- globals->cGlobals.params->vtMgr,
- globals,
- CHANNEL_NONE,
- sendOnCloseGTK );
+ mem_stream_make( MEMPOOL globals->cGlobals.params->vtMgr,
+ &globals->cGlobals, CHANNEL_NONE,
+ sendOnClose );
server_initClientConnection( globals->cGlobals.game.server,
stream );
}
@@ -926,7 +921,7 @@ handle_resend( GtkWidget* XP_UNUSED(widget), GtkAppGlobals* globals )
{
CommsCtxt* comms = globals->cGlobals.game.comms;
if ( comms != NULL ) {
- comms_resendAll( comms );
+ comms_resendAll( comms, XP_TRUE );
}
} /* handle_resend */
@@ -1747,8 +1742,8 @@ gtk_util_makeStreamFromAddr(XW_UtilCtxt* uc, XP_PlayerAddr channelNo )
XWStreamCtxt* stream = mem_stream_make( MEMPOOL
globals->cGlobals.params->vtMgr,
- uc->closure, channelNo,
- sendOnCloseGTK );
+ &globals->cGlobals, channelNo,
+ sendOnClose );
return stream;
} /* gtk_util_makeStreamFromAddr */
@@ -2204,7 +2199,7 @@ gtk_socket_changed( void* closure, int oldSock, int newSock, void** storage )
/* A hack for the bluetooth case. */
CommsCtxt* comms = globals->cGlobals.game.comms;
if ( (comms != NULL) && (comms_getConType(comms) == COMMS_CONN_BT) ) {
- comms_resendAll( comms );
+ comms_resendAll( comms, XP_FALSE );
}
LOG_RETURN_VOID();
} /* gtk_socket_changed */
@@ -2270,15 +2265,6 @@ gtk_socket_acceptor( int listener, Acceptor func, CommonGlobals* globals,
}
} /* gtk_socket_acceptor */
-static void
-sendOnCloseGTK( XWStreamCtxt* stream, void* closure )
-{
- GtkAppGlobals* globals = closure;
-
- XP_LOGF( "sendOnClose called" );
- (void)comms_send( globals->cGlobals.game.comms, stream );
-} /* sendOnClose */
-
static void
drop_msg_toggle( GtkWidget* toggle, GtkAppGlobals* globals )
{
diff --git a/xwords4/linux/linuxmain.c b/xwords4/linux/linuxmain.c
index 0b4b6d7e1..82b7d5c04 100644
--- a/xwords4/linux/linuxmain.c
+++ b/xwords4/linux/linuxmain.c
@@ -196,6 +196,14 @@ catOnClose( XWStreamCtxt* stream, void* XP_UNUSED(closure) )
free( buffer );
} /* catOnClose */
+void
+sendOnClose( XWStreamCtxt* stream, void* closure )
+{
+ CommonGlobals* cGlobals = (CommonGlobals*)closure;
+ XP_LOGF( "%s called with msg of len %d", __func__, stream_getSize(stream) );
+ (void)comms_send( cGlobals->game.comms, stream );
+}
+
void
catGameHistory( CommonGlobals* cGlobals )
{
@@ -1593,9 +1601,6 @@ main( int argc, char** argv )
mainParams.allowPeek = XP_TRUE;
mainParams.showRobotScores = XP_FALSE;
mainParams.useMmap = XP_TRUE;
-#ifdef XWFEATURE_DEVID
- mainParams.devID = "";
-#endif
char* envDictPath = getenv( "XW_DICTSPATH" );
if ( !!envDictPath ) {
diff --git a/xwords4/linux/linuxmain.h b/xwords4/linux/linuxmain.h
index bae629d47..44cf263e5 100644
--- a/xwords4/linux/linuxmain.h
+++ b/xwords4/linux/linuxmain.h
@@ -60,6 +60,8 @@ XP_UCHAR* strFromStream( XWStreamCtxt* stream );
void catGameHistory( CommonGlobals* cGlobals );
void catOnClose( XWStreamCtxt* stream, void* closure );
+void sendOnClose( XWStreamCtxt* stream, void* closure );
+
void catFinalScores( const CommonGlobals* cGlobals, XP_S16 quitter );
XP_Bool file_exists( const char* fileName );
XWStreamCtxt* streamFromFile( CommonGlobals* cGlobals, char* name,
diff --git a/xwords4/linux/linuxutl.c b/xwords4/linux/linuxutl.c
index 196551219..650c0c892 100644
--- a/xwords4/linux/linuxutl.c
+++ b/xwords4/linux/linuxutl.c
@@ -353,20 +353,37 @@ linux_util_getDevID( XW_UtilCtxt* uc, DevIDType* typ )
if ( !!cGlobals->params->rDevID ) {
*typ = ID_TYPE_RELAY;
result = cGlobals->params->rDevID;
- } else {
+ } else if ( !!cGlobals->params->devID ) {
*typ = ID_TYPE_LINUX;
result = cGlobals->params->devID;
+ } else {
+ *typ = ID_TYPE_NONE;
+ result = NULL;
}
return result;
}
static void
-linux_util_deviceRegistered( XW_UtilCtxt* XP_UNUSED(uc),
+linux_util_deviceRegistered( XW_UtilCtxt* uc, DevIDType typ,
const XP_UCHAR* idRelay )
{
- /* Script discon_ok2.sh is grepping for this in logs, so don't change
- it! */
- XP_LOGF( "%s: new id: %s", __func__, idRelay );
+ /* Script discon_ok2.sh is grepping for these strings in logs, so don't
+ change them! */
+ CommonGlobals* cGlobals = (CommonGlobals*)uc->closure;
+ switch( typ ) {
+ case ID_TYPE_NONE: /* error case */
+ XP_LOGF( "%s: id rejected", __func__ );
+ cGlobals->params->rDevID = cGlobals->params->devID = NULL;
+ break;
+ case ID_TYPE_RELAY:
+ if ( 0 < strlen( idRelay ) ) {
+ XP_LOGF( "%s: new id: %s", __func__, idRelay );
+ }
+ break;
+ default:
+ XP_ASSERT(0);
+ break;
+ }
}
#endif
diff --git a/xwords4/linux/scripts/discon_ok2.sh b/xwords4/linux/scripts/discon_ok2.sh
index fbaa29f2d..7abda93ab 100755
--- a/xwords4/linux/scripts/discon_ok2.sh
+++ b/xwords4/linux/scripts/discon_ok2.sh
@@ -29,6 +29,7 @@ declare -A PIDS
declare -A APPS
declare -A NEW_ARGS
declare -A ARGS
+declare -A ARGS_DEVID
declare -A ROOMS
declare -A FILES
declare -A LOGS
@@ -190,13 +191,14 @@ build_cmds() {
PARAMS="$PARAMS --drop-nth-packet $DROP_N $PLAT_PARMS"
# PARAMS="$PARAMS --savefail-pct 10"
[ -n "$SEED" ] && PARAMS="$PARAMS --seed $RANDOM"
- # PARAMS="$PARAMS --devid LINUX_TEST_$(printf %.5d ${COUNTER})"
PARAMS="$PARAMS $PUBLIC"
ARGS[$COUNTER]=$PARAMS
ROOMS[$COUNTER]=$ROOM
FILES[$COUNTER]=$FILE
LOGS[$COUNTER]=$LOG
PIDS[$COUNTER]=0
+ ARGS_DEVID[$COUNTER]=""
+ update_devid_cmd $COUNTER
print_cmdline $COUNTER
@@ -239,7 +241,7 @@ read_resume_cmds() {
launch() {
LOG=${LOGS[$1]}
APP="${APPS[$1]}"
- PARAMS="${NEW_ARGS[$1]} ${ARGS[$1]}"
+ PARAMS="${NEW_ARGS[$1]} ${ARGS[$1]} ${ARGS_DEVID[$1]}"
exec $APP $PARAMS >/dev/null 2>>$LOG
}
@@ -277,6 +279,7 @@ close_device() {
unset LOGS[$ID]
unset ROOMS[$ID]
unset APPS[$ID]
+ unset ARGS_DEVID[$ID]
}
OBITS=""
@@ -381,16 +384,42 @@ increment_drop() {
fi
}
-set_relay_devid() {
+get_relayid() {
KEY=$1
- CMD=${ARGS[$KEY]}
- if [ "$CMD" != "${CMD/--devid //}" ]; then
- RELAY_ID=$(grep 'deviceRegistered: new id: ' ${LOGS[$KEY]} | tail -n 1)
- if [ -n "$RELAY_ID" ]; then
- RELAY_ID=$(echo $RELAY_ID | sed 's,^.*new id: ,,')
- # turn --devid into --rdevid $RELAY_ID
- ARGS[$KEY]=$(echo $CMD | sed 's,^\(.*\)--devid[ ]\+[^ ]\+\(.*\)$,\1--rdevid $RELAY_ID\2,')
+ RELAY_ID=$(grep 'deviceRegistered: new id: ' ${LOGS[$KEY]} | tail -n 1)
+ if [ -n "$RELAY_ID" ]; then
+ RELAY_ID=$(echo $RELAY_ID | sed 's,^.*new id: ,,')
+ else
+ usage "new id string not in $LOG"
+ fi
+ echo $RELAY_ID
+}
+
+update_devid_cmd() {
+ KEY=$1
+ HELP="$(${APPS[$KEY]} --help 2>&1 || true)"
+ if echo $HELP | grep -q '\-\-devid'; then
+ CMD="--devid LINUX_TEST_$(printf %.5d ${KEY})"
+ LOG=${LOGS[$KEY]}
+ if [ -z "${ARGS_DEVID[$KEY]}" -o ! -e $LOG ]; then # upgrade or first run
+ :
+ else
+ # otherwise, we should have successfully registered. If
+ # we have AND the reg has been rejected, make without
+ # --rdevid so will reregister.
+ LAST_GOOD=$(grep -h -n 'linux_util_deviceRegistered: new id' $LOG | tail -n 1 | sed 's,:.*$,,')
+ LAST_REJ=$(grep -h -n 'linux_util_deviceRegistered: id rejected' $LOG | tail -n 1 | sed 's,:.*$,,')
+ # echo "LAST_GOOD: $LAST_GOOD; LAST_REJ: $LAST_REJ"
+ if [ -z "$LAST_GOOD" ]; then # not yet registered
+ :
+ elif [ -z "$LAST_REJ" ]; then
+ CMD="$CMD --rdevid $(get_relayid $KEY)"
+ elif [ "$LAST_REJ" -lt "$LAST_GOOD" ]; then # registered and not more recently rejected
+ CMD="$CMD --rdevid $(get_relayid $KEY)"
+ fi
fi
+ # echo $CMD
+ ARGS_DEVID[$KEY]=$CMD
fi
}
@@ -423,7 +452,7 @@ run_cmds() {
PIDS[$KEY]=0
ROOM_PIDS[$ROOM]=0
[ "$DROP_N" -ge 0 ] && increment_drop $KEY
- # set_relay_devid $KEY
+ update_devid_cmd $KEY
check_game $KEY
fi
done
diff --git a/xwords4/relay/Makefile b/xwords4/relay/Makefile
index 0c5b4e18e..891941db9 100644
--- a/xwords4/relay/Makefile
+++ b/xwords4/relay/Makefile
@@ -45,6 +45,7 @@ CPPFLAGS += -DSPAWN_SELF -g -Wall \
-I $(shell pg_config --includedir) \
-DSVN_REV=\"$(shell cat $(GITINFO) 2>/dev/null || echo -n $(HASH) )\"
# CPPFLAGS += -DDO_HTTP
+# CPPFLAGS += -DHAVE_STIME
# turn on semaphore debugging
# CPPFLAGS += -DDEBUG_LOCKS
diff --git a/xwords4/relay/cref.cpp b/xwords4/relay/cref.cpp
index a825557b3..0ec0b64d3 100644
--- a/xwords4/relay/cref.cpp
+++ b/xwords4/relay/cref.cpp
@@ -627,6 +627,7 @@ CookieRef::handleEvents()
/* Assumption: has mutex!!!! */
while ( m_eventQueue.size () > 0 ) {
XW_RELAY_STATE nextState;
+ DBMgr::DevIDRelay devID;
CRefEvent evt = m_eventQueue.front();
m_eventQueue.pop_front();
@@ -642,7 +643,6 @@ CookieRef::handleEvents()
case XWA_SEND_CONNRSP:
{
HostID hid;
- DBMgr::DevIDRelay devID;
if ( increasePlayerCounts( &evt, false, &hid, &devID ) ) {
setAllConnectedTimer();
sendResponse( &evt, true, &devID );
@@ -668,8 +668,8 @@ CookieRef::handleEvents()
/* break; */
case XWA_SEND_RERSP:
- increasePlayerCounts( &evt, true, NULL, NULL );
- sendResponse( &evt, false, NULL );
+ increasePlayerCounts( &evt, true, NULL, &devID );
+ sendResponse( &evt, false, &devID );
sendAnyStored( &evt );
postCheckAllHere();
break;
@@ -891,13 +891,7 @@ CookieRef::increasePlayerCounts( CRefEvent* evt, bool reconn, HostID* hidp,
DevIDType devIDType = evt->u.con.devID->m_devIDType;
// does client support devID
if ( ID_TYPE_NONE != devIDType ) {
- // have we not already converted it?
- if ( ID_TYPE_RELAY == devIDType ) {
- devID = (DBMgr::DevIDRelay)strtoul( evt->u.con.devID->m_devIDString.c_str(),
- NULL, 16 );
- } else {
- devID = DBMgr::Get()->RegisterDevice( evt->u.con.devID );
- }
+ devID = DBMgr::Get()->RegisterDevice( evt->u.con.devID );
}
*devIDp = devID;
}
@@ -1070,20 +1064,34 @@ CookieRef::sendResponse( const CRefEvent* evt, bool initial,
memcpy( bufp, connName, len );
bufp += len;
- if ( initial ) {
- // we always write at least empty string
- char idbuf[MAX_DEVID_LEN + 1] = {0};
+ // we always write at least empty string
- // If client supports devid, and we have one (response case), write it as
- // 8-byte hex string plus a length byte -- but only if we didn't already
- // receive it.
- if ( !!devID && ID_TYPE_RELAY < evt->u.con.devID->m_devIDType ) {
+ // If client supports devid, and we have one (response case), write it as
+ // 8-byte hex string plus a length byte -- but only if we didn't already
+ // receive it.
+
+ // there are three possibilities: it sent us a platform-specific ID and we
+ // need to return the relay version; or it sent us a valid relay version;
+ // or it sent us an invalid one (for whatever reason, e.g. we've wiped the
+ // devices table entry for a problematic GCM id to force reregistration.)
+ // In the first case, we return the new relay version. In the second, we
+ // return that the type is ID_TYPE_RELAY but don't bother with the version
+ // string; and in the third, we return ID_TYPE_NONE.
+
+ if ( DBMgr::DEVID_NONE == *devID ) { // first case
+ *bufp++ = ID_TYPE_NONE;
+ } else {
+ *bufp++ = ID_TYPE_RELAY;
+
+ // Write an empty string if the client passed the ID to us, or the id
+ // if it's new to the client.
+ char idbuf[MAX_DEVID_LEN + 1];
+ if ( !!ID_TYPE_RELAY < evt->u.con.devID->m_devIDType ) {
len = snprintf( idbuf, sizeof(idbuf), "%.8X", *devID );
assert( len < sizeof(idbuf) );
+ } else {
+ len = 0;
}
-
- len = strlen( idbuf );
- assert( len <= MAX_DEVID_LEN );
*bufp++ = (char)len;
if ( 0 < len ) {
memcpy( bufp, idbuf, len );
diff --git a/xwords4/relay/crefmgr.cpp b/xwords4/relay/crefmgr.cpp
index f86288d93..9adb5df07 100644
--- a/xwords4/relay/crefmgr.cpp
+++ b/xwords4/relay/crefmgr.cpp
@@ -616,13 +616,13 @@ SafeCref::SafeCref( const char* cookie, int socket, int clientVersion,
/* REconnect case */
SafeCref::SafeCref( const char* connName, const char* cookie, HostID hid,
- int socket, int clientVersion, int nPlayersH, int nPlayersS,
- unsigned short gameSeed, int langCode,
+ int socket, int clientVersion, DevID* devID, int nPlayersH,
+ int nPlayersS, unsigned short gameSeed, int langCode,
bool wantsPublic, bool makePublic )
: m_cinfo( NULL )
, m_mgr( CRefMgr::Get() )
, m_clientVersion( clientVersion )
- , m_devID( NULL )
+ , m_devID( devID )
, m_isValid( false )
{
CidInfo* cinfo;
diff --git a/xwords4/relay/crefmgr.h b/xwords4/relay/crefmgr.h
index e605b88f0..63e061f28 100644
--- a/xwords4/relay/crefmgr.h
+++ b/xwords4/relay/crefmgr.h
@@ -177,8 +177,8 @@ class SafeCref {
bool makePublic );
/* for reconnect */
SafeCref( const char* connName, const char* cookie, HostID hid,
- int socket, int clientVersion, int nPlayersH, int nPlayersS,
- unsigned short gameSeed, int langCode,
+ int socket, int clientVersion, DevID* devID, int nPlayersH,
+ int nPlayersS, unsigned short gameSeed, int langCode,
bool wantsPublic, bool makePublic );
SafeCref( const char* const connName );
SafeCref( CookieID cid, bool failOk = false );
diff --git a/xwords4/relay/dbmgr.cpp b/xwords4/relay/dbmgr.cpp
index 600d320e8..099403e8d 100644
--- a/xwords4/relay/dbmgr.cpp
+++ b/xwords4/relay/dbmgr.cpp
@@ -47,6 +47,7 @@ static void formatParams( char* paramValues[], int nParams, const char* fmt,
static int here_less_seed( const char* seeds, int perDeviceSum,
unsigned short seed );
static void destr_function( void* conn );
+static void string_printf( string& str, const char* fmt, ... );
/* static */ DBMgr*
DBMgr::Get()
@@ -127,11 +128,11 @@ DBMgr::FindGame( const char* connName, char* cookieBuf, int bufLen,
const char* fmt = "SELECT cid, room, lang, nTotal, nPerDevice, dead FROM "
GAMES_TABLE " WHERE connName = '%s'"
" LIMIT 1";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName );
- logf( XW_LOGINFO, "query: %s", query );
+ string query;
+ string_printf( query, fmt, connName );
+ logf( XW_LOGINFO, "query: %s", query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
@@ -234,11 +235,11 @@ DBMgr::AllDevsAckd( const char* const connName )
{
const char* cmd = "SELECT ntotal=sum_array(nperdevice) AND 'A'=ALL(ack) from " GAMES_TABLE
" WHERE connName='%s'";
- char query[256];
- snprintf( query, sizeof(query), cmd, connName );
- logf( XW_LOGINFO, "query: %s", query );
+ string query;
+ string_printf( query, cmd, connName );
+ logf( XW_LOGINFO, "query: %s", query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
int nTuples = PQntuples( result );
assert( nTuples <= 1 );
bool full = nTuples == 1 && 't' == PQgetvalue( result, 0, 0 )[0];
@@ -253,13 +254,16 @@ DBMgr::DevIDRelay
DBMgr::RegisterDevice( const DevID* host )
{
DBMgr::DevIDRelay devID;
- assert( host->m_devIDType != ID_TYPE_RELAY );
+ assert( host->m_devIDType != ID_TYPE_NONE );
int ii;
bool success;
// if it's already present, just return
devID = getDevID( host );
- if ( DEVID_NONE == devID ) {
+
+ // If it's not present *and* of type ID_TYPE_RELAY, we can do nothing.
+ // Fail.
+ if ( DEVID_NONE == devID && ID_TYPE_RELAY < host->m_devIDType ) {
// loop until we're successful inserting the unique key. Ship with this
// coming from random, but test with increasing values initially to make
// sure duplicates are detected.
@@ -314,10 +318,11 @@ DBMgr::AddDevice( const char* connName, HostID curID, int clientVersion,
}
assert( newID <= 4 );
- char devIDBuf[512] = {0};
+ string devIDBuf;
if ( DEVID_NONE != devID ) {
- snprintf( devIDBuf, sizeof(devIDBuf),
- "devids[%d] = %d, ", newID, devID );
+ string_printf( devIDBuf, "devids[%d] = %d, ", newID, devID );
+ } else {
+ assert( 0 == strlen(devIDBuf.c_str()) );
}
const char* fmt = "UPDATE " GAMES_TABLE " SET nPerDevice[%d] = %d,"
@@ -325,11 +330,11 @@ DBMgr::AddDevice( const char* connName, HostID curID, int clientVersion,
" seeds[%d] = %d, addrs[%d] = \'%s\', %s"
" mtimes[%d]='now', ack[%d]=\'%c\'"
" WHERE connName = '%s'";
- char query[1024];
- snprintf( query, sizeof(query), fmt, newID, nToAdd, newID, clientVersion,
- newID, seed, newID, inet_ntoa(addr), devIDBuf,
- newID, newID, ackd?'A':'a', connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, newID, nToAdd, newID, clientVersion,
+ newID, seed, newID, inet_ntoa(addr), devIDBuf.c_str(),
+ newID, newID, ackd?'A':'a', connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
@@ -339,11 +344,11 @@ DBMgr::AddDevice( const char* connName, HostID curID, int clientVersion,
void
DBMgr::NoteAckd( const char* const connName, HostID id )
{
- char query[256];
const char* fmt = "UPDATE " GAMES_TABLE " SET ack[%d]='A'"
" WHERE connName = '%s'";
- snprintf( query, sizeof(query), fmt, id, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, id, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@@ -353,9 +358,9 @@ DBMgr::RmDeviceByHid( const char* connName, HostID hid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET nPerDevice[%d] = 0, "
"seeds[%d] = 0, ack[%d]='-', mtimes[%d]='now' WHERE connName = '%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, hid, hid, hid, hid, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, hid, hid, hid, hid, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
return execSql( query );
}
@@ -368,10 +373,10 @@ DBMgr::HIDForSeed( const char* const connName, unsigned short seed )
const char* fmt = "SELECT seeds FROM " GAMES_TABLE
" WHERE connName = '%s'"
" AND %d = ANY(seeds)";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName, seed );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
- PGresult* result = PQexec( getThreadConn(), query );
+ string query;
+ string_printf( query, fmt, connName, seed );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
snprintf( seeds, sizeof(seeds), "%s", PQgetvalue( result, 0, 0 ) );
}
@@ -415,10 +420,10 @@ DBMgr::HaveDevice( const char* connName, HostID hid, int seed )
bool found = false;
const char* fmt = "SELECT * from " GAMES_TABLE
" WHERE connName = '%s' AND seeds[%d] = %d";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName, hid, seed );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
- PGresult* result = PQexec( getThreadConn(), query );
+ string query;
+ string_printf( query, fmt, connName, hid, seed );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
found = 1 == PQntuples( result );
PQclear( result );
return found;
@@ -429,9 +434,9 @@ DBMgr::AddCID( const char* const connName, CookieID cid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET cid = %d "
" WHERE connName = '%s' AND cid IS NULL";
- char query[256];
- snprintf( query, sizeof(query), fmt, cid, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, cid, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
bool result = execSql( query );
logf( XW_LOGINFO, "%s(cid=%d)=>%d", __func__, cid, result );
@@ -443,9 +448,9 @@ DBMgr::ClearCID( const char* connName )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET cid = null "
"WHERE connName = '%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@@ -457,9 +462,9 @@ DBMgr::RecordSent( const char* const connName, HostID hid, int nBytes )
const char* fmt = "UPDATE " GAMES_TABLE " SET"
" nsent = nsent + %d, mtimes[%d] = 'now'"
" WHERE connName = '%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, nBytes, hid, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, nBytes, hid, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@@ -468,23 +473,19 @@ void
DBMgr::RecordSent( const int* msgIDs, int nMsgIDs )
{
if ( nMsgIDs > 0 ) {
- char buf[1024];
- unsigned int offset = 0;
- offset = snprintf( buf, sizeof(buf), "SELECT connname,hid,sum(msglen)"
- " FROM " MSGS_TABLE " WHERE id IN (" );
+ string query( "SELECT connname,hid,sum(msglen)"
+ " FROM " MSGS_TABLE " WHERE id IN (" );
for ( int ii = 0; ; ) {
- offset += snprintf( &buf[offset], sizeof(buf) - offset, "%d,",
- msgIDs[ii] );
- assert( offset < sizeof(buf) );
+ string_printf( query, "%d", msgIDs[ii] );
if ( ++ii == nMsgIDs ) {
- --offset; /* back over comma */
break;
+ } else {
+ query.append( "," );
}
}
- offset += snprintf( &buf[offset], sizeof(buf) - offset,
- ") GROUP BY connname,hid" );
+ query.append( ") GROUP BY connname,hid" );
- PGresult* result = PQexec( getThreadConn(), buf );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( PGRES_TUPLES_OK == PQresultStatus( result ) ) {
int ntuples = PQntuples( result );
for ( int ii = 0; ii < ntuples; ++ii ) {
@@ -504,9 +505,9 @@ DBMgr::RecordAddress( const char* const connName, HostID hid,
assert( hid >= 0 && hid <= 4 );
const char* fmt = "UPDATE " GAMES_TABLE " SET addrs[%d] = \'%s\'"
" WHERE connName = '%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, hid, inet_ntoa(addr), connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, hid, inet_ntoa(addr), connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@@ -516,11 +517,11 @@ DBMgr::GetPlayerCounts( const char* const connName, int* nTotal, int* nHere )
{
const char* fmt = "SELECT ntotal, sum_array(nperdevice) FROM " GAMES_TABLE
" WHERE connName = '%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
*nTotal = atoi( PQgetvalue( result, 0, 0 ) );
*nHere = atoi( PQgetvalue( result, 0, 1 ) );
@@ -530,11 +531,11 @@ DBMgr::GetPlayerCounts( const char* const connName, int* nTotal, int* nHere )
void
DBMgr::KillGame( const char* const connName, int hid )
{
- const char* fmt = "UPDATE " GAMES_TABLE " SET dead = TRUE,"
- " nperdevice[%d] = - nperdevice[%d]"
- " WHERE connName = '%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, hid, hid, connName );
+ const char* fmt = "UPDATE " GAMES_TABLE " SET dead = TRUE,"
+ " nperdevice[%d] = - nperdevice[%d]"
+ " WHERE connName = '%s'";
+ string query;
+ string_printf( query, fmt, hid, hid, connName );
execSql( query );
}
@@ -556,11 +557,11 @@ DBMgr::PublicRooms( int lang, int nPlayers, int* nNames, string& names )
" AND nTotal>sum_array(nPerDevice)"
" AND nTotal = %d";
- char query[256];
- snprintf( query, sizeof(query), fmt, lang, nPlayers );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, lang, nPlayers );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
int nTuples = PQntuples( result );
for ( int ii = 0; ii < nTuples; ++ii ) {
names.append( PQgetvalue( result, ii, 0 ) );
@@ -579,12 +580,16 @@ DBMgr::PendingMsgCount( const char* connName, int hid )
{
int count = 0;
const char* fmt = "SELECT COUNT(*) FROM " MSGS_TABLE
- " WHERE connName = '%s' AND hid = %d";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName, hid );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ " WHERE connName = '%s' AND hid = %d "
+#ifdef HAVE_STIME
+ "AND stime IS NULL"
+#endif
+ ;
+ string query;
+ string_printf( query, fmt, connName, hid );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
count = atoi( PQgetvalue( result, 0, 0 ) );
}
@@ -592,6 +597,12 @@ DBMgr::PendingMsgCount( const char* connName, int hid )
return count;
}
+bool
+DBMgr::execSql( const string& query )
+{
+ return execSql( query.c_str() );
+}
+
bool
DBMgr::execSql( const char* const query )
{
@@ -609,11 +620,11 @@ DBMgr::readArray( const char* const connName, int arr[] ) /* len 4 */
{
const char* fmt = "SELECT nPerDevice FROM " GAMES_TABLE " WHERE connName='%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
const char* arrStr = PQgetvalue( result, 0, 0 );
sscanf( arrStr, "{%d,%d,%d,%d}", &arr[0], &arr[1], &arr[2], &arr[3] );
@@ -625,11 +636,11 @@ DBMgr::getDevID( const char* connName, int hid )
{
DBMgr::DevIDRelay devID;
const char* fmt = "SELECT devids[%d] FROM " GAMES_TABLE " WHERE connName='%s'";
- char query[256];
- snprintf( query, sizeof(query), fmt, hid, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, hid, connName );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
devID = (DBMgr::DevIDRelay)strtoul( PQgetvalue( result, 0, 0 ), NULL, 10 );
PQclear( result );
@@ -641,17 +652,24 @@ DBMgr::getDevID( const DevID* devID )
{
DBMgr::DevIDRelay rDevID = DEVID_NONE;
DevIDType devIDType = devID->m_devIDType;
+ string query;
assert( ID_TYPE_NONE < devIDType );
const char* asStr = devID->m_devIDString.c_str();
if ( ID_TYPE_RELAY == devIDType ) {
- rDevID = strtoul( asStr, NULL, 16 );
+ // confirm it's there
+ DBMgr::DevIDRelay cur = strtoul( asStr, NULL, 16 );
+ if ( DEVID_NONE != cur ) {
+ const char* fmt = "SELECT id FROM " DEVICES_TABLE " WHERE id=%d";
+ string_printf( query, fmt, cur );
+ }
} else {
const char* fmt = "SELECT id FROM " DEVICES_TABLE " WHERE devtype=%d and devid = '%s'";
- char query[512];
- snprintf( query, sizeof(query), fmt, devIDType, asStr );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string_printf( query, fmt, devIDType, asStr );
+ }
- PGresult* result = PQexec( getThreadConn(), query );
+ if ( 0 < query.size() ) {
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 >= PQntuples( result ) );
if ( 1 == PQntuples( result ) ) {
rDevID = (DBMgr::DevIDRelay)strtoul( PQgetvalue( result, 0, 0 ), NULL, 10 );
@@ -673,18 +691,20 @@ int
DBMgr::CountStoredMessages( const char* const connName, int hid )
{
const char* fmt = "SELECT count(*) FROM " MSGS_TABLE
- " WHERE connname = '%s' ";
+ " WHERE connname = '%s' "
+#ifdef HAVE_STIME
+ "AND stime IS NULL"
+#endif
+ ;
- char query[256];
- int len = snprintf( query, sizeof(query), fmt, connName );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ string query;
+ string_printf( query, fmt, connName );
if ( hid != -1 ) {
- snprintf( &query[len], sizeof(query)-len, "AND hid = %d",
- hid );
+ string_printf( query, "AND hid = %d", hid );
}
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
int count = atoi( PQgetvalue( result, 0, 0 ) );
PQclear( result );
@@ -712,18 +732,13 @@ DBMgr::StoreMessage( const char* const connName, int hid,
len, &newLen );
assert( NULL != bytes );
- char query[1024];
- size_t siz = snprintf( query, sizeof(query), fmt, connName, hid,
- devID, bytes, len );
+ string query;
+ string_printf( query, fmt, connName, hid, devID, bytes, len );
PQfreemem( bytes );
- if ( siz < sizeof(query) ) {
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
- execSql( query );
- } else {
- logf( XW_LOGERROR, "%s: buffer too small", __func__ );
- }
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
+ execSql( query );
}
bool
@@ -732,12 +747,16 @@ DBMgr::GetNthStoredMessage( const char* const connName, int hid,
int* msgID )
{
const char* fmt = "SELECT id, msg, msglen FROM " MSGS_TABLE
- " WHERE connName = '%s' AND hid = %d ORDER BY id LIMIT 1 OFFSET %d";
- char query[256];
- snprintf( query, sizeof(query), fmt, connName, hid, nn );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ " WHERE connName = '%s' AND hid = %d "
+#ifdef HAVE_STIME
+ "AND stime IS NULL "
+#endif
+ "ORDER BY id LIMIT 1 OFFSET %d";
+ string query;
+ string_printf( query, fmt, connName, hid, nn );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
- PGresult* result = PQexec( getThreadConn(), query );
+ PGresult* result = PQexec( getThreadConn(), query.c_str() );
int nTuples = PQntuples( result );
assert( nTuples <= 1 );
@@ -774,22 +793,29 @@ void
DBMgr::RemoveStoredMessages( const int* msgIDs, int nMsgIDs )
{
if ( nMsgIDs > 0 ) {
- char ids[1024];
+ string ids;
size_t len = 0;
int ii;
for ( ii = 0; ; ) {
- len += snprintf( ids + len, sizeof(ids) - len, "%d,", msgIDs[ii] );
+ string_printf( ids, "%d", msgIDs[ii] );
assert( len < sizeof(ids) );
if ( ++ii == nMsgIDs ) {
- ids[len-1] = '\0'; /* overwrite last comma */
break;
+ } else {
+ ids.append( "," );
}
}
- const char* fmt = "DELETE from " MSGS_TABLE " WHERE id in (%s)";
- char query[1024];
- snprintf( query, sizeof(query), fmt, ids );
- logf( XW_LOGINFO, "%s: query: %s", __func__, query );
+ const char* fmt =
+#ifdef HAVE_STIME
+ "UPDATE " MSGS_TABLE " SET stime='now' "
+#else
+ "DELETE FROM " MSGS_TABLE
+#endif
+ " WHERE id IN (%s)";
+ string query;
+ string_printf( query, fmt, ids.c_str() );
+ logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
}
@@ -852,3 +878,29 @@ DBMgr::getThreadConn( void )
}
return conn;
}
+
+/* From stack overflow, toward a snprintf with an expanding buffer.
+ */
+static void
+string_printf( string& str, const char* fmt, ... )
+{
+ const int origsiz = str.size();
+ int newsiz = 100;
+ va_list ap;
+ for ( ; ; ) {
+ str.resize( origsiz + newsiz );
+
+ va_start( ap, fmt );
+ int len = vsnprintf( (char *)str.c_str() + origsiz, newsiz, fmt, ap );
+ va_end( ap );
+
+ if ( len > newsiz ) { // needs more space
+ newsiz = len + 1;
+ } else if ( -1 == len ) {
+ assert(0); // should be impossible
+ } else {
+ str.resize( origsiz + len );
+ break;
+ }
+ }
+}
diff --git a/xwords4/relay/dbmgr.h b/xwords4/relay/dbmgr.h
index 46af13f02..bc4918dc7 100644
--- a/xwords4/relay/dbmgr.h
+++ b/xwords4/relay/dbmgr.h
@@ -105,6 +105,7 @@ class DBMgr {
private:
DBMgr();
+ bool execSql( const string& query );
bool execSql( const char* const query ); /* no-results query */
void readArray( const char* const connName, int arr[] );
DevIDRelay getDevID( const char* connName, int hid );
diff --git a/xwords4/relay/scripts/gcm_loop.py b/xwords4/relay/scripts/gcm_loop.py
index f7c524053..666df85dd 100755
--- a/xwords4/relay/scripts/gcm_loop.py
+++ b/xwords4/relay/scripts/gcm_loop.py
@@ -6,7 +6,7 @@
#
# Depends on the gcm module
-import getpass, sys, gcm, psycopg2, time, signal
+import getpass, sys, gcm, psycopg2, time, signal, shelve
from time import gmtime, strftime
# I'm not checking my key in...
@@ -25,108 +25,147 @@ import mykey
# contact list if it is the target of at least one message in the msgs
# table.
+k_shelfFile = "gcm_loop.shelf"
+k_SENT = 'SENT'
g_con = None
+g_sent = None
g_debug = False
g_skipSend = False # for debugging
-DEVTYPE = 3 # 3 == GCM
+DEVTYPE_GCM = 3 # 3 == GCM
LINE_LEN = 76
def init():
+ global g_sent
try:
con = psycopg2.connect(database='xwgames', user=getpass.getuser())
except psycopg2.DatabaseError, e:
print 'Error %s' % e
sys.exit(1)
+
+ shelf = shelve.open( k_shelfFile )
+ if k_SENT in shelf: g_sent = shelf[k_SENT]
+ else: g_sent = {}
+ shelf.close();
+ if g_debug: print 'g_sent:', g_sent
+
return con
-def getPendingMsgs( con ):
+# WHERE stime IS NULL
+
+def getPendingMsgs( con, typ ):
cur = con.cursor()
- cur.execute("SELECT id, devid FROM msgs WHERE devid IN (SELECT id FROM devices WHERE devtype=%d)" % DEVTYPE)
+ query = """SELECT id, devid FROM msgs
+ WHERE devid IN (SELECT id FROM devices WHERE devtype=%d and NOT unreg)
+ AND NOT connname IN (SELECT connname FROM games WHERE dead); """
+ cur.execute(query % typ)
result = cur.fetchall()
if g_debug: print "getPendingMsgs=>", result
return result
-def asGCMIds(con, devids):
+def unregister( gcmid ):
+ global g_con
+ print "unregister(", gcmid, ")"
+ query = "UPDATE devices SET unreg=TRUE WHERE id = '%s'" % gcmid
+ g_con.cursor().execute( query )
+
+def asGCMIds(con, devids, typ):
cur = con.cursor()
query = "SELECT devid FROM devices WHERE devtype = %d AND id IN (%s)" \
- % (DEVTYPE, ",".join([str(y) for y in devids]))
+ % (typ, ",".join([str(y) for y in devids]))
cur.execute( query )
return [elem[0] for elem in cur.fetchall()]
-def notifyGCM( devids ):
- instance = gcm.GCM( mykey.myKey )
- data = { 'getMoves': True,
- # 'title' : 'Msg from Darth',
- # 'msg' : "I am your father, Luke.",
- }
- # JSON request
-
- response = instance.json_request( registration_ids = devids,
- data = data )
-
- if 'errors' in response:
- for error, reg_ids in response.items():
- print error
+def notifyGCM( devids, typ ):
+ if typ == DEVTYPE_GCM:
+ instance = gcm.GCM( mykey.myKey )
+ data = { 'getMoves': True, }
+ response = instance.json_request( registration_ids = devids,
+ data = data,
+ )
+ if 'errors' in response:
+ response = response['errors']
+ if 'NotRegistered' in response:
+ for gcmid in response['NotRegistered']:
+ unregister( gcmid )
+ else:
+ print "got some kind of error"
+ else:
+ if g_debug: print 'no errors:', response
else:
- print 'no errors'
+ print "not sending to", len(devids), "devices because typ ==", typ
def shouldSend(val):
- pow = 1
- while pow < val:
- pow *= 2
- return pow == val
+ return val == 1
+ # pow = 1
+ # while pow < val:
+ # pow *= 3
+ # return pow == val
# given a list of msgid, devid lists, figure out which messages should
# be sent/resent now and mark them as sent. Backoff is based on
# msgids: if the only messages a device has pending have been seen
# before, backoff applies.
-def targetsAfterBackoff( msgs, sent ):
- targets = []
+def targetsAfterBackoff( msgs ):
+ global g_sent
+ targets = {}
for row in msgs:
msgid = row[0]
- if not msgid in sent:
- sent[msgid] = 0
- sent[msgid] += 1
- if shouldSend( sent[msgid] ):
- targets.append( row[1] )
- return targets
+ devid = row[1]
+ if not msgid in g_sent:
+ g_sent[msgid] = 0
+ g_sent[msgid] += 1
+ if shouldSend( g_sent[msgid] ):
+ targets[devid] = True
+ return targets.keys()
# devids is an array of (msgid, devid) tuples
-def pruneSent( devids, sent ):
- if g_debug: print "pruneSent: before:", sent
- lenBefore = len(sent)
+def pruneSent( devids ):
+ global g_sent
+ if g_debug: print "pruneSent: before:", g_sent
+ lenBefore = len(g_sent)
msgids = []
for row in devids:
msgids.append(row[0])
- for msgid in sent.keys():
+ for msgid in g_sent.keys():
if not msgid in msgids:
- del sent[msgid]
- if g_debug: print "pruneSent: after:", sent
- return sent
+ del g_sent[msgid]
+ if g_debug: print "pruneSent: after:", g_sent
+
+def cleanup():
+ global g_con, g_sent
+ if g_con:
+ g_con.close()
+ g_con = None
+ shelf = shelve.open( k_shelfFile )
+ shelf[k_SENT] = g_sent
+ shelf.close();
def handleSigTERM( one, two ):
print 'handleSigTERM called: ', one, two
- global g_con
- if g_con:
- g_con.close()
- g_con = None
+ cleanup()
def usage():
- print "usage:", sys.argv[0], "[--loop]"
+ print "usage:", sys.argv[0], "[--loop ] [--type typ] [--verbose]"
sys.exit();
def main():
- global g_con
+ global g_con, g_sent, g_debug
loopInterval = 0
g_con = init()
emptyCount = 0
+ typ = DEVTYPE_GCM
ii = 1
while ii < len(sys.argv):
arg = sys.argv[ii]
if arg == '--loop':
- ii = ii + 1
+ ii += 1
loopInterval = float(sys.argv[ii])
+ elif arg == '--type':
+ ii += 1
+ typ = int(sys.argv[ii])
+ elif arg == '--verbose':
+ g_debug = True
else:
usage()
ii = ii + 1
@@ -134,30 +173,29 @@ def main():
signal.signal( signal.SIGTERM, handleSigTERM )
signal.signal( signal.SIGINT, handleSigTERM )
- sent = {}
while g_con:
- devids = getPendingMsgs( g_con )
+ if g_debug: print
+ devids = getPendingMsgs( g_con, typ )
if 0 < len(devids):
- targets = targetsAfterBackoff( devids, sent )
+ targets = targetsAfterBackoff( devids )
if 0 < len(targets):
if 0 < emptyCount: print ""
emptyCount = 0
- print strftime("%Y-%m-%d %H:%M:%S", gmtime()),
+ print strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
print "devices needing notification:", targets
- if not g_skipSend:
- notifyGCM( asGCMIds( g_con, targets ) )
- pruneSent( devids, sent )
- else:
+ notifyGCM( asGCMIds( g_con, targets, typ ), typ )
+ pruneSent( devids )
+ elif g_debug: print "no targets after backoff"
+ else:
+ emptyCount += 1
+ if (0 == (emptyCount%5)) and not g_debug:
sys.stdout.write('.')
sys.stdout.flush()
- emptyCount = emptyCount + 1
- if 0 == (emptyCount % LINE_LEN): print ""
+ if 0 == (emptyCount % (LINE_LEN*5)): print ""
if 0 == loopInterval: break
time.sleep( loopInterval )
- if g_debug: print
- if g_con:
- g_con.close()
+ cleanup()
##############################################################################
if __name__ == '__main__':
diff --git a/xwords4/relay/scripts/gcm_msg.py b/xwords4/relay/scripts/gcm_msg.py
index d90e4137a..a043bfac1 100755
--- a/xwords4/relay/scripts/gcm_msg.py
+++ b/xwords4/relay/scripts/gcm_msg.py
@@ -1,10 +1,13 @@
#!/usr/bin/python
-import sys, gcm, psycopg2
+import sys, gcm, psycopg2, json
# I'm not checking my key in...
import mykey
+def usage():
+ print 'usage:', sys.argv[0], '[--to ] msg'
+ sys.exit()
def msgViaGCM( devid, msg ):
instance = gcm.GCM( mykey.myKey )
@@ -14,18 +17,30 @@ def msgViaGCM( devid, msg ):
response = instance.json_request( registration_ids = [devid],
data = data )
-
if 'errors' in response:
- for error, reg_ids in response.items():
- print error
+ response = response['errors']
+ if 'NotRegistered' in response:
+ ids = response['NotRegistered']
+ for id in ids:
+ print 'need to remove "', id, '" from db'
else:
print 'no errors'
-
def main():
+ to = None
msg = sys.argv[1]
- print 'got "%s"' % msg
- msgViaGCM( mykey.myBlaze, msg )
+ if msg == '--to':
+ to = sys.argv[2]
+ msg = sys.argv[3]
+ elif 2 < len(sys.argv):
+ usage()
+ if not to in mykey.devids.keys():
+ print 'Unknown --to param;', to, 'not in', ','.join(mykey.devids.keys())
+ usage()
+ if not to: usage()
+ devid = mykey.devids[to]
+ print 'sending: "%s" to' % msg, to
+ msgViaGCM( devid, msg )
##############################################################################
if __name__ == '__main__':
diff --git a/xwords4/relay/scripts/showgames.php b/xwords4/relay/scripts/showgames.php
index b3372b279..0c0e09999 100644
--- a/xwords4/relay/scripts/showgames.php
+++ b/xwords4/relay/scripts/showgames.php
@@ -98,6 +98,7 @@ $cols = array( new Column("dead", "D", "capitalize", false ),
new Column("clntVers", "CV", "identity", true ),
new Column("nperdevice", "NP", "identity", true ),
new Column("ack", "A", "identity", true ),
+ new Column("devids", "DevIDs", "identity", true ),
new Column("nsent", "Sent", "identity", false ),
new Column("addrs", "Dev. addr", "ip_to_host", true ),
new Column("ctime", "Created", "print_date", false ),
diff --git a/xwords4/relay/xwrelay.cpp b/xwords4/relay/xwrelay.cpp
index 3105992f7..b01bd0c5c 100644
--- a/xwords4/relay/xwrelay.cpp
+++ b/xwords4/relay/xwrelay.cpp
@@ -242,6 +242,21 @@ getNetString( unsigned char** bufpp, const unsigned char* end, string& out )
return success;
}
+static void
+getDevID( unsigned char** bufpp, const unsigned char* end,
+ unsigned short flags, DevID* devID )
+{
+ if ( XWRELAY_PROTO_VERSION_CLIENTID <= flags ) {
+ unsigned char devIDType = 0;
+ if ( getNetByte( bufpp, end, &devIDType ) && 0 != devIDType ) {
+ if ( getNetString( bufpp, end, devID->m_devIDString )
+ && 0 < devID->m_devIDString.length() ) {
+ devID->m_devIDType = (DevIDType)devIDType;
+ }
+ }
+ }
+}
+
#ifdef RELAY_HEARTBEAT
static bool
processHeartbeat( unsigned char* buf, int bufLen, int socket )
@@ -381,16 +396,7 @@ processConnect( unsigned char* bufp, int bufLen, int socket, in_addr& addr )
&& getNetByte( &bufp, end, &langCode ) ) {
DevID devID;
- if ( XWRELAY_PROTO_VERSION_CLIENTID <= flags ) {
- unsigned char devIDType = 0;
- if ( getNetByte( &bufp, end, &devIDType )
- && 0 != devIDType ) {
- if ( getNetString( &bufp, end, devID.m_devIDString )
- && 0 < devID.m_devIDString.length() ) {
- devID.m_devIDType = (DevIDType)devIDType;
- }
- }
- }
+ getDevID( &bufp, end, flags, &devID );
logf( XW_LOGINFO, "%s(): langCode=%d; nPlayersT=%d; "
"wantsPublic=%d; seed=%.4X",
@@ -449,9 +455,12 @@ processReconnect( unsigned char* bufp, int bufLen, int socket, in_addr& addr )
&& getNetByte( &bufp, end, &langCode )
&& readStr( &bufp, end, connName, sizeof(connName) ) ) {
+ DevID devID;
+ getDevID( &bufp, end, flags, &devID );
+
SafeCref scr( connName[0]? connName : NULL,
- cookie, srcID, socket, clientVersion, nPlayersH,
- nPlayersT, gameSeed, langCode,
+ cookie, srcID, socket, clientVersion, &devID,
+ nPlayersH, nPlayersT, gameSeed, langCode,
wantsPublic, makePublic );
success = scr.Reconnect( socket, srcID, nPlayersH, nPlayersT,
gameSeed, addr, &err );
diff --git a/xwords4/relay/xwrelay.sh b/xwords4/relay/xwrelay.sh
index 91ae66d14..5a53b697d 100755
--- a/xwords4/relay/xwrelay.sh
+++ b/xwords4/relay/xwrelay.sh
@@ -68,6 +68,7 @@ id SERIAL
,connName VARCHAR(64)
,hid INTEGER
,ctime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+,stime TIMESTAMP DEFAULT NULL
,devid INTEGER
,msg BYTEA
,msglen INTEGER
@@ -81,6 +82,7 @@ id INTEGER UNIQUE PRIMARY KEY
,devType INTEGER
,devid TEXT
,ctime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+,unreg BOOLEAN DEFAULT FALSE
);
EOF
}