diff --git a/.gitignore b/.gitignore index e4fc03066..444620918 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ TAGS core *.apk xwords_4.4.0.0* +gcm_loop.shelf diff --git a/xwords4/android/XWords4/AndroidManifest.xml b/xwords4/android/XWords4/AndroidManifest.xml index f8a817f36..a1446e4b8 100644 --- a/xwords4/android/XWords4/AndroidManifest.xml +++ b/xwords4/android/XWords4/AndroidManifest.xml @@ -22,11 +22,11 @@ to come from a domain that you own or have control over. --> - + @@ -115,15 +115,31 @@ - + - + + + + + + + + + + + + + + + diff --git a/xwords4/android/XWords4/build.xml b/xwords4/android/XWords4/build.xml index b1d8ad988..d1536010c 100644 --- a/xwords4/android/XWords4/build.xml +++ b/xwords4/android/XWords4/build.xml @@ -61,6 +61,7 @@ diff --git a/xwords4/android/XWords4/jni/utilwrapper.c b/xwords4/android/XWords4/jni/utilwrapper.c index 7d6a24e48..d29d610c0 100644 --- a/xwords4/android/XWords4/jni/utilwrapper.c +++ b/xwords4/android/XWords4/jni/utilwrapper.c @@ -552,6 +552,7 @@ static const XP_UCHAR* and_util_getDevID( XW_UtilCtxt* uc, DevIDType* typ ) { const XP_UCHAR* result = NULL; + *typ = ID_TYPE_NONE; UTIL_CBK_HEADER( "getDevID", "([B)Ljava/lang/String;" ); jbyteArray jbarr = makeByteArray( env, 1, NULL ); jstring jresult = (*env)->CallObjectMethod( env, util->jutil, mid, jbarr ); @@ -581,11 +582,12 @@ and_util_getDevID( XW_UtilCtxt* uc, DevIDType* typ ) } static void -and_util_deviceRegistered( XW_UtilCtxt* uc, const XP_UCHAR* idRelay ) +and_util_deviceRegistered( XW_UtilCtxt* uc, DevIDType typ, + const XP_UCHAR* idRelay ) { - UTIL_CBK_HEADER( "deviceRegistered", "(Ljava/lang/String;)V" ); + UTIL_CBK_HEADER( "deviceRegistered", "(ILjava/lang/String;)V" ); jstring jstr = (*env)->NewStringUTF( env, idRelay ); - (*env)->CallVoidMethod( env, util->jutil, mid, jstr ); + (*env)->CallVoidMethod( env, util->jutil, mid, typ, jstr ); deleteLocalRef( env, jstr ); UTIL_CBK_TAIL(); } diff --git a/xwords4/android/XWords4/jni/xwjni.c b/xwords4/android/XWords4/jni/xwjni.c index c74f1d402..535405ece 100644 --- a/xwords4/android/XWords4/jni/xwjni.c +++ b/xwords4/android/XWords4/jni/xwjni.c @@ -974,9 +974,7 @@ and_send_on_close( XWStreamCtxt* stream, void* closure ) JNIState* state = (JNIState*)globals->state; XP_ASSERT( !!state->game.comms ); - if ( stream_getSize( stream ) > 0 ) { - comms_send( state->game.comms, stream ); - } + comms_send( state->game.comms, stream ); } JNIEXPORT void JNICALL @@ -1307,14 +1305,16 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1changeDict JNIEXPORT void JNICALL Java_org_eehouse_android_xw4_jni_XwJNI_comms_1resendAll -( JNIEnv* env, jclass C, jint gamePtr, jboolean thenAck ) +( JNIEnv* env, jclass C, jint gamePtr, jboolean force, jboolean thenAck ) { XWJNI_START(); CommsCtxt* comms = state->game.comms; XP_ASSERT( !!comms ); - (void)comms_resendAll( comms ); + (void)comms_resendAll( comms, force ); if ( thenAck ) { +#ifdef XWFEATURE_COMMSACK comms_ackAny( comms ); +#endif } XWJNI_END(); } diff --git a/xwords4/android/XWords4/project.properties b/xwords4/android/XWords4/project.properties index 797fb4fc3..c1fd41ab1 100644 --- a/xwords4/android/XWords4/project.properties +++ b/xwords4/android/XWords4/project.properties @@ -10,4 +10,4 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=android-7 +target=android-8 diff --git a/xwords4/android/XWords4/res/layout/game_list_item.xml b/xwords4/android/XWords4/res/layout/game_list_item.xml index 1a6ab2cb1..771af6471 100644 --- a/xwords4/android/XWords4/res/layout/game_list_item.xml +++ b/xwords4/android/XWords4/res/layout/game_list_item.xml @@ -3,95 +3,114 @@ - + - + - - - - - - - - - - - - - - + android:visibility="gone" + > - - + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + diff --git a/xwords4/android/XWords4/res/layout/game_list_tmp.xml b/xwords4/android/XWords4/res/layout/game_list_tmp.xml deleted file mode 100644 index 00ec641a9..000000000 --- a/xwords4/android/XWords4/res/layout/game_list_tmp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/xwords4/android/XWords4/res/raw/changes b/xwords4/android/XWords4/res/raw/changes index 509bcac36..98a8b8f2b 100644 --- a/xwords4/android/XWords4/res/raw/changes +++ b/xwords4/android/XWords4/res/raw/changes @@ -5,12 +5,17 @@ -Crosswords 4.4 beta 54 release +Crosswords 4.4 beta 58 release + +

New with this release

    +
  • 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_tiles key_show_arrow + key_square_tiles key_explain_robot key_skip_confirm key_sort_tiles @@ -30,7 +31,6 @@ key_clr_bonushint key_relay_host - key_redir_host key_relay_port2 key_update_url key_update_prerel @@ -68,9 +68,10 @@ key_sms_phones key_connstat_data key_dev_id - key_gcm_regid + key_gcmvers_regid key_relay_regid key_checked_sms + key_default_group key_notagain_sync key_notagain_chat @@ -102,9 +103,12 @@ eehouse.org - + eehouse.org + /and/ + application/x-xwordsinvite + + http://eehouse.org/and_wordlists - //%1$s/newgame.php Update checks URL http://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? Decline Downloading %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.) +

    + + +EOF; +} + +function printAndroid() { +print << +

    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.

    + +
    + +
    +EOF; +} + + +/********************************************************************** + * Main() + **********************************************************************/ +$agent = $_SERVER['HTTP_USER_AGENT']; +$onAndroid = false; +for ( $ii = 0; $ii < count($g_androidStrings) && !$onAndroid; ++$ii ) { + $needle = $g_androidStrings[$ii]; + $onAndroid = false !== stripos( $agent, $needle ); +} +$onFire = false !== stripos( $agent, 'silk' ); + +printHead(); +if ( /*true || */ $onFire || $onAndroid ) { + printAndroid(); +} else { + printNonAndroid($agent); +} +printTail(); + + +?> diff --git a/xwords4/android/scripts/gen_gcmid.sh b/xwords4/android/scripts/gen_gcmid.sh index 9ca58833a..bf719f2ab 100755 --- a/xwords4/android/scripts/gen_gcmid.sh +++ b/xwords4/android/scripts/gen_gcmid.sh @@ -1,8 +1,11 @@ #!/bin/sh +set -e -u + +GCM_SENDER_ID=${GCM_SENDER_ID:-""} + if [ -z "$GCM_SENDER_ID" ]; then - echo "GCM_SENDER_ID not in env" - exit 1 + echo "GCM_SENDER_ID empty; GCM use will be disabled" >&2 fi cat < "; + } +} + +$g_androidStrings = array( "android", ); $scheme = "newxwgame"; $host = "10.0.2.2"; @@ -10,33 +34,46 @@ $lang = $_REQUEST["lang"]; $room = $_REQUEST["room"]; $np = $_REQUEST["np"]; $id = $_REQUEST["id"]; +$wl = $_REQUEST["wl"]; -$content = "0; url=$scheme://$host?room=$room&lang=$lang&np=$np"; +$agent = $_SERVER['HTTP_USER_AGENT']; +$onAndroid = false; +for ( $ii = 0; $ii < count($g_androidStrings) && !$onAndroid; ++$ii ) { + $needle = $g_androidStrings[$ii]; + $onAndroid = 0 != stripos( $agent, $needle ); +} +$onFire = 0 != stripos( $agent, 'silk' ); + +$localurl = "$scheme://$host?room=$room&lang=$lang&np=$np"; if ( $id != "" ) { - $content .= "&id=$id"; + $localurl .= "&id=$id"; +} +if ( $wl != "" ) { + $localurl .= "&wl=$wl"; } +if ( $onAndroid || $onFire ) { print << -Crosswords SMS redirect - +Crosswords Invite redirect
    - + -

    redirecting to Crosswords....

    - -

    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. +

    Tap this link to launch Crosswords with + your new game. +

    +

    If this fails it's probably because you don't have a new enough + version of Crosswords installed.

    +
    @@ -44,4 +81,77 @@ print << + + +Crosswords Invite redirect + + + +

    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: + +

      +
    1. Open Crosswords, and navigate to the main Games List screen
    2. +
    3. Choose "Add game", either from the menu or the button at the bottom.
    4. +
    5. Under "New Networked game", choose "Configure first".
    6. +
    7. $langText
    8. +
    9. As the room name, enter "$room".
    10. +
    11. Make sure the total number of players shown is $np and that only one of them is not an "Off-device player".
    12. +
    13. Now tap the "Play game" button at the bottom (above the keyboard). Your new game should open and connect.
    14. +

    +

    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 }