diff --git a/xwords4/android/XWords4/AndroidManifest.xml b/xwords4/android/XWords4/AndroidManifest.xml index 640303a36..1b634d862 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. --> - + @@ -134,14 +134,12 @@ /> - - - - - - - - + + + + + + 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 20c716fa5..b9613677d 100644 --- a/xwords4/android/XWords4/res/raw/changes +++ b/xwords4/android/XWords4/res/raw/changes @@ -5,18 +5,22 @@ -Crosswords 4.4 beta 56 release -
    New with this release -
  • Improve invitations: no more redirection through a website, and - confirm before creating duplicate games
  • -
  • (For sideloading users only) No more browser involvement in - updates: app can launch the installer directly to update - itself
  • -
  • Remove notifications when their games are deleted
  • +Crosswords 4.4 beta 57 release + +

    New with this release

    +
      +
    • Include new game information as an attachment in email invites + for use on devices that don't dispatch URLs correctly in received + email (e.g. some by HTC)
    • +
    • Show final scores alert whenever a finished game is opened -- to + make it more clear that it's finished
    • +
    • Fix flickering in main screen (games list)
    • +
    • Add option, off by default, to keep rack tiles square even when + the screen is large enough that they can be taller
    -
      Next up -
    • One more idea for improving invitations
    • +

      Next up

      +
      • Allow grouping of games in collapsible user-defined categores: "Games with Kati", "Finished games", etc.
      diff --git a/xwords4/android/XWords4/res/values/app_name.xml b/xwords4/android/XWords4/res/values/app_name.xml index 844bb12a6..095652a34 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 56 + 4.4 beta 57 diff --git a/xwords4/android/XWords4/res/values/common_rsrc.xml b/xwords4/android/XWords4/res/values/common_rsrc.xml index 860b43b11..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 diff --git a/xwords4/android/XWords4/res/values/strings.xml b/xwords4/android/XWords4/res/values/strings.xml index 41a15afcc..f6aab751a 100644 --- a/xwords4/android/XWords4/res/values/strings.xml +++ b/xwords4/android/XWords4/res/values/strings.xml @@ -1233,8 +1233,10 @@ 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 (or the full link below) to accept my invitation and - join this game. + 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) @@ -2154,4 +2156,13 @@ Moving is impossible until there is more than one group. + + + 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 86fdece79..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/DBHelper.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java index b44021f29..cf6a4d6bd 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java @@ -60,7 +60,7 @@ 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"; @@ -100,7 +100,7 @@ public class DBHelper extends SQLiteOpenHelper { ,SEED, "INTEGER" ,DICTLANG, "INTEGER" ,DICTLIST, "TEXT" - ,SMSPHONE, "TEXT" + ,SMSPHONE, "TEXT" // unused ,SCORES, "TEXT" ,CHAT_HISTORY, "TEXT" ,GAMEID, "INTEGER" 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 7ace05033..a9495ef3b 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 @@ -372,7 +375,7 @@ public class DBUtils { 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 ) @@ -447,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(); @@ -469,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(); @@ -574,7 +572,7 @@ public class DBUtils { return result; } - public static String[] getRelayIDs( Context context, boolean noMsgs ) + public static String[] getRelayIDs( Context context, long[][] rowIDs ) { String[] result = null; initDB( context ); @@ -582,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; } @@ -675,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 ) { @@ -695,16 +698,16 @@ public class DBUtils { 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() ); @@ -722,13 +725,13 @@ public class DBUtils { updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values ); setCached( rowid, null ); // force reread - if ( -1 != rowid ) { // Is this possible? PENDING - notifyListeners( rowid ); + 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 ); @@ -756,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 ); @@ -771,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 ) { @@ -1099,6 +1118,7 @@ public class DBUtils { public static void loadDB( Context context ) { + clearRowIDsCache(); copyGameDB( context, false ); } @@ -1381,12 +1401,29 @@ public class DBUtils { } } - 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/DictUtils.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DictUtils.java index 126410cfd..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 ); } + // 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/DlgDelegate.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DlgDelegate.java index b0e642bf1..05ace973a 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/DlgDelegate.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/DlgDelegate.java @@ -242,7 +242,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/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 abb37a82a..320594293 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 @@ -21,8 +21,6 @@ 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; @@ -44,7 +42,6 @@ import java.util.Set; 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; @@ -52,242 +49,35 @@ import org.eehouse.android.xw4.DBUtils.GameGroupInfo; public class GameListAdapter implements ExpandableListAdapter { private Context m_context; + private ExpandableListView 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 ) - { - if ( -1 != m_rowid ) { - m_cb.itemLoaded( m_rowid ); - } - } - } // class LoadItemTask - - public GameListAdapter( Context context, Handler handler, LoadItemCB cb ) { + public GameListAdapter( Context context, ExpandableListView 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 ); } - public void inval( long rowid ) - { - synchronized( m_viewsCache ) { - m_viewsCache.remove( rowid ); - } - } + // public void inval( long rowid ) + // { + // synchronized( m_viewsCache ) { + // m_viewsCache.remove( rowid ); + // } + // } public void expandGroups( ExpandableListView view ) { @@ -301,26 +91,26 @@ public class GameListAdapter implements ExpandableListAdapter { } } - public void setField( String field ) - { - int[] ids = { - R.string.game_summary_field_empty - ,R.string.game_summary_field_language - ,R.string.game_summary_field_opponents - ,R.string.game_summary_field_state - }; - int result = -1; - for ( int id : ids ) { - if ( m_context.getString( id ).equals( field ) ) { - result = id; - break; - } - } - if ( m_fieldID != result ) { - m_viewsCache.clear(); - m_fieldID = result; - } - } + // public void setField( String field ) + // { + // int[] ids = { + // R.string.game_summary_field_empty + // ,R.string.game_summary_field_language + // ,R.string.game_summary_field_opponents + // ,R.string.game_summary_field_state + // }; + // int result = -1; + // for ( int id : ids ) { + // if ( m_context.getString( id ).equals( field ) ) { + // result = id; + // break; + // } + // } + // if ( m_fieldID != result ) { + // m_viewsCache.clear(); + // m_fieldID = result; + // } + // } public long getRowIDFor( int group, int child ) { @@ -473,39 +263,57 @@ public class GameListAdapter implements ExpandableListAdapter { public void registerDataSetObserver( DataSetObserver obs ){} public void unregisterDataSetObserver( DataSetObserver obs ){} - private View getItem( final long rowid ) + // private View getItem( final long rowid ) + // { + // 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 ); + // } + // } + // } + // @Override + // public int getCount() { + // return DBUtils.gamesList(m_context).length; + // } + + // // 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 ) + // { + // 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 ) { - 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 ); - } + 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(); } + } - if ( !haveLayout ) { - new LoadItemTask( m_context, rowid/*, ++m_taskCounter*/ ).execute(); + public void invalName( long rowid ) + { + GameListItem item = getItemFor( rowid ); + if ( null != item ) { + item.invalName(); } - - // 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 + } private long[] getRows( String group ) { @@ -538,9 +346,67 @@ public class GameListAdapter implements ExpandableListAdapter { return pos; } - private HashMap gameInfo() + public boolean setField( String fieldName ) { - return DBUtils.getGroups( m_context ); + 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; } -} \ No newline at end of file + 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 + ,R.string.game_summary_field_language + ,R.string.game_summary_field_opponents + ,R.string.game_summary_field_state + }; + int result = -1; + for ( int id : ids ) { + if ( m_context.getString( id ).equals( fieldName ) ) { + result = id; + break; + } + } + 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; + } +} 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 bbd5f0185..a471e1f40 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java @@ -46,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 ) @@ -242,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, @@ -298,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; } @@ -525,22 +408,6 @@ public class GameUtils { 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, @@ -566,18 +433,20 @@ public class GameUtils { 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 + if ( null != tmpdir ) { // no attachment + attach = makeJsonFor( tmpdir, room, inviteID, lang, + dict, nPlayers ); + } + + if ( null == attach ) { // no attachment intent.setType( "message/rfc822"); } else { - intent.setType( context.getString( R.string.invite_mime ) ); - - File attach = makeJsonFor( tmpdir, room, inviteID, lang, - dict, nPlayers ); + String mime = context.getString( R.string.invite_mime ); + intent.setType( mime ); Uri uri = Uri.fromFile( attach ); - DbgUtils.logf( "using file uri for attachment: %s", - uri.toString() ); intent.putExtra( Intent.EXTRA_STREAM, uri ); } @@ -684,7 +553,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 ); @@ -734,40 +602,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 ); - if ( 0 != gamePtr ) { - XwJNI.comms_resendAll( gamePtr, false, 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; } - } - XwJNI.comms_ackAny( gamePtr ); + 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 ); + // 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 ); + int flags = setFromFeedImpl( feedImpl ); + if ( GameSummary.MSG_FLAGS_NONE != flags ) { + draw = true; + DBUtils.setMsgFlags( rowid, flags ); + } } + lock.unlock(); } - lock.unlock(); } return draw; } // feedMessages @@ -781,52 +654,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, @@ -953,7 +819,7 @@ public class GameUtils { byte[] data = json.toString().getBytes(); File file = new File( dir, - String.format("invite_%s.json", room ) ); + String.format("invite_%s", room ) ); FileOutputStream fos = new FileOutputStream( file ); fos.write( data, 0, data.length ); fos.close(); 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 9104bd5e1..53e062129 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java @@ -75,6 +75,7 @@ public class GamesList extends XWExpandableListActivity 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; @@ -102,7 +103,6 @@ public class GamesList extends XWExpandableListActivity private String m_nameField; private NetLaunchInfo m_netLaunchInfo; private GameNamer m_namer; - // private String m_smsPhone; @Override protected Dialog onCreateDialog( int id ) @@ -177,11 +177,12 @@ public class GamesList extends XWExpandableListActivity getCheckedItemPosition(); String dict = m_sameLangDicts[pos]; dict = DictLangCache.stripCount( dict ); - GameUtils.replaceDicts( GamesList.this, - m_missingDictRowId, - m_missingDictName, - dict ); - launchGameIf(); + if ( GameUtils.replaceDicts( GamesList.this, + m_missingDictRowId, + m_missingDictName, + dict ) ) { + launchGameIf(); + } } }; dialog = new AlertDialog.Builder( this ) @@ -202,8 +203,7 @@ public class GamesList extends XWExpandableListActivity public void onClick( DialogInterface dlg, int item ) { String name = m_namer.getName(); DBUtils.setName( GamesList.this, m_rowid, name ); - m_adapter.inval( m_rowid ); - onContentChanged(); + m_adapter.invalName( m_rowid ); } }; dialog = buildNamerDlg( GameUtils.getName( this, m_rowid ), @@ -346,7 +346,9 @@ public class GamesList extends XWExpandableListActivity } }); - 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 ); @@ -355,6 +357,7 @@ public class GamesList extends XWExpandableListActivity startFirstHasDict( intent ); startNewNetGame( intent ); startHasGameID( intent ); + startHasRowID( intent ); askDefaultNameIf(); } // onCreate @@ -369,6 +372,7 @@ public class GamesList extends XWExpandableListActivity startFirstHasDict( intent ); startNewNetGame( intent ); startHasGameID( intent ); + startHasRowID( intent ); } @Override @@ -444,28 +448,25 @@ public class GamesList extends XWExpandableListActivity } // 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 @@ -480,12 +481,12 @@ public class GamesList extends XWExpandableListActivity 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 ) @@ -518,6 +519,7 @@ public class GamesList extends XWExpandableListActivity 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 ); @@ -526,7 +528,6 @@ public class GamesList extends XWExpandableListActivity 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: @@ -759,8 +760,7 @@ public class GamesList extends XWExpandableListActivity 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(); } @@ -859,9 +859,12 @@ public class GamesList extends XWExpandableListActivity } else if ( null != m_missingDictName ) { showDialog( WARN_NODICT_SUBST ); } else { - String dict = DictLangCache.getHaveLang( this, m_missingDictLang)[0]; - GameUtils.replaceDicts( this, m_missingDictRowId, null, dict ); - launchGameIf(); + String dict = + DictLangCache.getHaveLang( this, m_missingDictLang)[0]; + if ( GameUtils.replaceDicts( this, m_missingDictRowId, + null, dict ) ) { + launchGameIf(); + } } } return hasDicts; @@ -878,7 +881,6 @@ public class GamesList extends XWExpandableListActivity } } } - onContentChanged(); } } @@ -893,7 +895,7 @@ public class GamesList extends XWExpandableListActivity if ( null != rowids ) { for ( long rowid : rowids ) { if ( GameUtils.gameDictsHere( this, rowid ) ) { - GameUtils.launchGame( this, rowid ); + launchGame( rowid ); break outer; } } @@ -951,7 +953,7 @@ public class GamesList extends XWExpandableListActivity { long[] rowids = DBUtils.getRowIDsFor( this, gameID ); if ( null != rowids && 0 < rowids.length ) { - GameUtils.launchGame( this, rowids[0] ); + launchGame( rowids[0] ); } } @@ -963,6 +965,16 @@ public class GamesList extends XWExpandableListActivity } } + 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 ) ) { @@ -975,9 +987,9 @@ public class GamesList extends XWExpandableListActivity 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(); } } @@ -1019,10 +1031,20 @@ public class GamesList extends XWExpandableListActivity 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 ); - GameUtils.launchGame( this, rowid, true ); + launchGame( rowid, true ); } public static void onGameDictDownload( Context context, Intent intent ) @@ -1054,6 +1076,20 @@ public class GamesList extends XWExpandableListActivity 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 ); 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 4e4d21686..5d2a776eb 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/MultiService.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/MultiService.java @@ -42,7 +42,7 @@ public class MultiService { 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 @@ -64,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; 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 e2f79b4ed..878316cf1 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetLaunchInfo.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetLaunchInfo.java @@ -73,7 +73,7 @@ public class NetLaunchInfo { if ( null != data ) { String scheme = data.getScheme(); try { - if ( "content".equals(scheme) ) { + if ( "content".equals(scheme) || "file".equals(scheme) ) { Assert.assertNotNull( context ); ContentResolver resolver = context.getContentResolver(); InputStream is = resolver.openInputStream( data ); 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 4f900a76d..75d3d11b0 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetUtils.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NetUtils.java @@ -130,8 +130,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 { @@ -141,6 +140,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 ); @@ -263,4 +263,16 @@ public class NetUtils { DbgUtils.logf( "sendToRelay: null msgs" ); } } // sendToRelay + + private static int sumStrings( final String[] strs ) + { + int len = 0; + if ( null != strs ) { + for ( String str : strs ) { + len += str.length(); + } + } + 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 039aaa23d..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 ) { @@ -357,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 ); } } @@ -378,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/RelayService.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayService.java index c312a8fc8..5da48cd30 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayService.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/RelayService.java @@ -74,45 +74,40 @@ public class RelayService extends Service { } } } - - 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 ); - setupNotification( relayIDs ); + String[] tmp = new String[idsWMsgs.size()]; + idsWMsgs.toArray( tmp ); + setupNotification( tmp ); } sink.send( this ); } 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 6734e1220..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 ) { 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 4744e48ab..6d74998aa 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/Utils.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/Utils.java @@ -40,6 +40,8 @@ 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; @@ -345,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 ) { 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 18670d379..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; @@ -192,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 6c05dc270..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,12 +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 ATTACH_SUPPORTED = 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/XWListActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWListActivity.java index e2adf4364..5b843f8f1 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; @@ -187,7 +187,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 dce8adae8..b966a051e 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWPrefs.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/XWPrefs.java @@ -83,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 = 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 24b82e392..e39adc389 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 @@ -34,6 +34,7 @@ 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; @@ -94,6 +95,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 +122,7 @@ 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 GameLock m_lock; private Context m_context; private CurGameInfo m_gi; private Handler m_handler; @@ -142,7 +144,7 @@ public class JNIThread extends Thread { } public JNIThread( int gamePtr, CurGameInfo gi, SyncedDraw drawer, - GameUtils.GameLock lock, Context context, Handler handler ) + GameLock lock, Context context, Handler handler ) { m_jniGamePtr = gamePtr; m_gi = gi; @@ -524,8 +526,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/scripts/and_index.php b/xwords4/android/scripts/and_index.php index baee28084..7d0a53cc1 100644 --- a/xwords4/android/scripts/and_index.php +++ b/xwords4/android/scripts/and_index.php @@ -30,16 +30,17 @@ 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 script to recognize" + . " 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.) +

      (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.)

      @@ -73,6 +74,11 @@ invite email (or text) and tap the link again.

      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.

      diff --git a/xwords4/common/game.c b/xwords4/common/game.c index f61e25ab2..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 ); 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/relay/scripts/gcm_loop.py b/xwords4/relay/scripts/gcm_loop.py index 0e9b48015..666df85dd 100755 --- a/xwords4/relay/scripts/gcm_loop.py +++ b/xwords4/relay/scripts/gcm_loop.py @@ -55,13 +55,19 @@ def init(): def getPendingMsgs( con, typ ): cur = con.cursor() query = """SELECT id, devid FROM msgs - WHERE devid IN (SELECT id FROM devices WHERE devtype=%d) + 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 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)" \ @@ -72,20 +78,15 @@ def asGCMIds(con, devids, typ): def notifyGCM( devids, typ ): if typ == DEVTYPE_GCM: instance = gcm.GCM( mykey.myKey ) - data = { 'getMoves': True, - # 'title' : 'Msg from Darth', - # 'msg' : "I am your father, Luke.", - } + data = { 'getMoves': True, } response = instance.json_request( registration_ids = devids, - # restricted_package_name = 'org.eehouse.android.xw4', data = data, - # collapse_key = 'NewMove', ) if 'errors' in response: response = response['errors'] if 'NotRegistered' in response: - for id in response['NotRegistered']: - print 'need to remove "', id, '" from db' + for gcmid in response['NotRegistered']: + unregister( gcmid ) else: print "got some kind of error" else: @@ -184,6 +185,7 @@ def main(): print "devices needing notification:", targets 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: