Merge branch 'android_branch' into android_groups

Conflicts:
	xwords4/android/XWords4/res/values/strings.xml
	xwords4/android/XWords4/src/org/eehouse/android/xw4/DBUtils.java
	xwords4/android/XWords4/src/org/eehouse/android/xw4/GameListAdapter.java
	xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java
	xwords4/android/XWords4/src/org/eehouse/android/xw4/XWApp.java
This commit is contained in:
Eric House 2012-12-18 06:35:07 -08:00
commit 8a58492389
44 changed files with 1333 additions and 828 deletions

View file

@ -22,11 +22,11 @@
to come from a domain that you own or have control over. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.eehouse.android.xw4"
android:versionCode="48"
android:versionCode="49"
android:versionName="@string/app_version"
>
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="7" />
<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="8" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -134,14 +134,12 @@
/>
</intent-filter>
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.VIEW" /> -->
<!-- <category android:name="android.intent.category.DEFAULT" /> -->
<!-- <category android:name="android.intent.category.BROWSABLE" /> -->
<!-- <data android:mimeType="*/*" -->
<!-- android:scheme="content" -->
<!-- /> -->
<!-- </intent-filter> -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="@string/invite_mime" />
</intent-filter>
</activity>
<!-- downloading dicts -->

View file

@ -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

View file

@ -3,95 +3,114 @@
<!-- top-level layout is hozontal, with an image and another layout -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:longClickable="true"
android:focusable="true"
android:clickable="true"
android:background="@android:drawable/list_selector_background"
>
<org.eehouse.android.xw4.GameListItem
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:longClickable="true"
android:focusable="true"
android:clickable="true"
android:background="@android:drawable/list_selector_background"
>
<ImageView android:id="@+id/msg_marker"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical|center_horizontal"
android:layout_weight="0"
/>
<TextView android:id="@+id/view_unloaded"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/game_list_tmp"
/>
<!-- this layout is vertical, holds everything but the status
icon[s] (plural later) -->
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<!-- This is the game name and expander -->
<LinearLayout android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<org.eehouse.android.xw4.ExpiringTextView
android:id="@+id/game_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
<ImageButton android:id="@+id/expander"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/expander_ic_maximized"
/>
</LinearLayout>
<!-- This is everything below the name (which can be hidden) -->
<LinearLayout android:id="@+id/hideable"
<LinearLayout android:id="@+id/view_loaded"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="4sp">
android:visibility="gone"
>
<!-- Player list plus connection status -->
<LinearLayout android:id="@+id/player_list"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_weight="1"
android:layout_marginRight="4dip"
/> <!-- end players column -->
<ImageView android:id="@+id/msg_marker"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical|center_horizontal"
android:layout_weight="0"
/>
<!-- holds right column. Could hold more... -->
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
>
<TextView android:id="@+id/modtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right"
/>
<TextView android:id="@+id/state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right"
/>
</LinearLayout>
<!-- this layout is vertical, holds everything but the status
icon[s] (plural later) -->
<LinearLayout android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<!-- This is the game name and expander -->
<LinearLayout android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<org.eehouse.android.xw4.ExpiringTextView
android:id="@+id/game_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
<ImageButton android:id="@+id/expander"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/expander_ic_maximized"
/>
</LinearLayout>
<!-- This is everything below the name (which can be hidden) -->
<LinearLayout android:id="@+id/hideable"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="4sp">
<!-- Player list plus connection status -->
<LinearLayout android:id="@+id/player_list"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_weight="1"
android:layout_marginRight="4dip"
/> <!-- end players column -->
<!-- holds right column. Could hold more... -->
<LinearLayout android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
>
<TextView android:id="@+id/modtime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right"
/>
<TextView android:id="@+id/state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right"
/>
</LinearLayout>
</LinearLayout>
<TextView android:id="@+id/role"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
/>
</LinearLayout>
</LinearLayout>
<TextView android:id="@+id/role"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|center_horizontal"
/>
</LinearLayout>
</LinearLayout>
</org.eehouse.android.xw4.GameListItem>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/game_list_tmp"
/>

View file

@ -5,18 +5,22 @@
</style>
</head>
<body>
<b>Crosswords 4.4 beta 56 release</b>
<ul>New with this release
<li>Improve invitations: no more redirection through a website, and
confirm before creating duplicate games</li>
<li>(For sideloading users only) No more browser involvement in
updates: app can launch the installer directly to update
itself</li>
<li>Remove notifications when their games are deleted</li>
<b>Crosswords 4.4 beta 57 release</b>
<h3>New with this release</h3>
<ul>
<li>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) </li>
<li>Show final scores alert whenever a finished game is opened -- to
make it more clear that it's finished</li>
<li>Fix flickering in main screen (games list)</li>
<li>Add option, off by default, to keep rack tiles square even when
the screen is large enough that they can be taller</li>
</ul>
<ul>Next up
<li>One more idea for improving invitations</li>
<h3>Next up</h3>
<ul>
<li>Allow grouping of games in collapsible user-defined categores: "Games with
Kati", "Finished games", etc.</li>
</ul>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_version">4.4 beta 56</string>
<string name="app_version">4.4 beta 57</string>
</resources>

View file

@ -6,6 +6,7 @@
<!-- prefs keys -->
<string name="key_color_tiles">key_color_tiles</string>
<string name="key_show_arrow">key_show_arrow</string>
<string name="key_square_tiles">key_square_tiles</string>
<string name="key_explain_robot">key_explain_robot</string>
<string name="key_skip_confirm">key_skip_confirm</string>
<string name="key_sort_tiles">key_sort_tiles</string>

View file

@ -1233,8 +1233,10 @@
encodings for the greater-than and less-than symbols which
are not legal in xml strings.)-->
<string name="invite_htmf">\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 @@
<string name="no_move_onegroup">Moving is impossible until there
is more than one group.</string>
<!-- Button shown in game over dialog triggering creation of new
game with the same players and parameters as the one that
just ended. -->
<string name="button_rematch">Rematch</string>
<string name="square_tiles">Square rack tiles</string>
<string name="square_tiles_summary">Even if they can be taller</string>
</resources>

View file

@ -132,6 +132,11 @@
android:summary="@string/show_arrow_summary"
android:defaultValue="true"
/>
<CheckBoxPreference android:key="@string/key_square_tiles"
android:title="@string/square_tiles"
android:summary="@string/square_tiles_summary"
android:defaultValue="false"
/>
<CheckBoxPreference android:key="@string/key_keep_screenon"
android:title="@string/keep_screenon"
android:summary="@string/keep_screenon_summary"

View file

@ -45,6 +45,14 @@ public class BTInviteActivity extends InviteActivity
private boolean m_firstScan;
private int m_checkCount;
public static void launchForResult( Activity activity, int nMissing,
int requestCode )
{
Intent intent = new Intent( activity, BTInviteActivity.class );
intent.putExtra( INTENT_KEY_NMISSING, nMissing );
activity.startActivityForResult( intent, requestCode );
}
@Override
protected void onCreate( Bundle savedInstanceState )
{
@ -57,7 +65,7 @@ public class BTInviteActivity extends InviteActivity
BTService.clearDevices( this, null ); // will return names
}
// BTService.BTEventListener interface
// MultiService.MultiEventListener interface
@Override
public void eventOccurred( MultiService.MultiEvent event, final Object ... args )
{

View file

@ -147,7 +147,7 @@ public class BTService extends Service {
}
}
public static void setListener( MultiService.BTEventListener li )
public static void setListener( MultiService.MultiEventListener li )
{
if ( XWApp.BTSUPPORTED ) {
if ( null == s_srcMgr ) {

View file

@ -75,7 +75,7 @@ public class BoardActivity extends XWActivity
private static final int PICK_TILE_REQUESTTRAY_BLK = DLG_OKONLY + 11;
private static final int DLG_USEDICT = DLG_OKONLY + 12;
private static final int DLG_GETDICT = DLG_OKONLY + 13;
private static final int GAME_OVER = DLG_OKONLY + 14;
private static final int CHAT_REQUEST = 1;
private static final int BT_INVITE_RESULT = 2;
@ -114,7 +114,7 @@ public class BoardActivity extends XWActivity
private BoardView m_view;
private int m_jniGamePtr;
private GameUtils.GameLock m_gameLock;
private GameLock m_gameLock;
private CurGameInfo m_gi;
private CommsTransport m_xport;
private Handler m_handler = null;
@ -165,6 +165,7 @@ public class BoardActivity extends XWActivity
private int m_missing;
private boolean m_haveInvited = false;
private boolean m_overNotShown;
private static BoardActivity s_this = null;
private static Class s_thisLocker = BoardActivity.class;
@ -188,6 +189,27 @@ public class BoardActivity extends XWActivity
return delivered;
}
public static boolean feedMessages( long rowid, byte[][] msgs )
{
boolean delivered = false;
Assert.assertNotNull( msgs );
synchronized( s_thisLocker ) {
if ( null != s_this ) {
Assert.assertNotNull( s_this.m_gi );
Assert.assertNotNull( s_this.m_gameLock );
Assert.assertNotNull( s_this.m_jniThread );
if ( rowid == s_this.m_rowid ) {
delivered = true; // even if no messages!
for ( byte[] msg : msgs ) {
s_this.m_jniThread.handle( JNICmd.CMD_RECEIVE, msg,
null );
}
}
}
}
return delivered;
}
private static void setThis( BoardActivity self )
{
synchronized( s_thisLocker ) {
@ -234,6 +256,7 @@ public class BoardActivity extends XWActivity
case DLG_OKONLY:
case DLG_BADWORDS:
case DLG_RETRY:
case GAME_OVER:
ab = new AlertDialog.Builder( this )
.setTitle( m_dlgTitle )
.setMessage( m_dlgBytes )
@ -246,6 +269,14 @@ public class BoardActivity extends XWActivity
}
};
ab.setNegativeButton( R.string.button_retry, lstnr );
} else if ( XWApp.REMATCH_SUPPORTED && GAME_OVER == id ) {
lstnr = new DialogInterface.OnClickListener() {
public void onClick( DialogInterface dlg,
int whichButton ) {
doRematch();
}
};
ab.setNegativeButton( R.string.button_rematch, lstnr );
}
dialog = ab.create();
Utils.setRemoveOnDismiss( this, dialog, id );
@ -499,7 +530,9 @@ public class BoardActivity extends XWActivity
Intent intent = getIntent();
m_rowid = intent.getLongExtra( GameUtils.INTENT_KEY_ROWID, -1 );
DbgUtils.logf( "BoardActivity: opening rowid %d", m_rowid );
m_haveInvited = intent.getBooleanExtra( GameUtils.INVITED, false );
m_overNotShown = true;
setBackgroundColor();
setKeepScreenOn();
@ -691,8 +724,13 @@ public class BoardActivity extends XWActivity
item.setTitle( R.string.board_menu_game_final );
}
if ( DeviceRole.SERVER_STANDALONE == m_gi.serverRole ) {
Utils.setItemVisible( menu, R.id.board_menu_game_resend, false );
Utils.setItemVisible( menu, R.id.gamel_menu_checkmoves, false );
}
return true;
}
} // onPrepareOptionsMenu
public boolean onOptionsItemSelected( MenuItem item )
{
@ -830,12 +868,12 @@ public class BoardActivity extends XWActivity
doSyncMenuitem();
break;
case BT_PICK_ACTION:
GameUtils.launchBTInviter( this, m_nMissingPlayers,
BT_INVITE_RESULT );
BTInviteActivity.launchForResult( this, m_nMissingPlayers,
BT_INVITE_RESULT );
break;
case SMS_PICK_ACTION:
GameUtils.launchSMSInviter( this, m_nMissingPlayers,
SMS_INVITE_RESULT );
SMSInviteActivity.launchForResult( this, m_nMissingPlayers,
SMS_INVITE_RESULT );
break;
case SMS_CONFIG_ACTION:
Utils.launchSettings( this );
@ -907,7 +945,7 @@ public class BoardActivity extends XWActivity
}
//////////////////////////////////////////////////
// BTService.BTEventListener interface
// MultiService.MultiEventListener interface
//////////////////////////////////////////////////
@Override
@SuppressWarnings("fallthrough")
@ -1635,7 +1673,7 @@ public class BoardActivity extends XWActivity
showDictGoneFinish();
} else {
Assert.assertNull( m_gameLock );
m_gameLock = new GameUtils.GameLock( m_rowid, true ).lock();
m_gameLock = new GameLock( m_rowid, true ).lock();
byte[] stream = GameUtils.savedGame( this, m_gameLock );
m_gi = new CurGameInfo( this );
@ -1693,6 +1731,11 @@ public class BoardActivity extends XWActivity
launchLookup( wordsToArray((String)msg.obj),
m_gi.dictLang );
break;
case JNIThread.GAME_OVER:
m_dlgBytes = (String)msg.obj;
m_dlgTitle = msg.arg1;
showDialog( GAME_OVER );
break;
}
}
};
@ -1722,8 +1765,18 @@ public class BoardActivity extends XWActivity
if ( 0 != (GameSummary.MSG_FLAGS_CHAT & flags) ) {
startChatActivity();
}
if ( 0 != (GameSummary.MSG_FLAGS_GAMEOVER & flags) ) {
m_jniThread.handle( JNICmd.CMD_POST_OVER );
if ( m_overNotShown ) {
boolean auto = false;
if ( 0 != (GameSummary.MSG_FLAGS_GAMEOVER & flags) ) {
m_gameOver = true;
} else if ( DBUtils.gameOver( this, m_rowid ) ) {
m_gameOver = true;
auto = true;
}
if ( m_gameOver ) {
m_overNotShown = false;
m_jniThread.handle( JNICmd.CMD_POST_OVER, auto );
}
}
if ( 0 != flags ) {
DBUtils.setMsgFlags( m_rowid, GameSummary.MSG_FLAGS_NONE );
@ -2074,4 +2127,11 @@ public class BoardActivity extends XWActivity
m_passwdEdit = (EditText)m_passwdLyt.findViewById( R.id.edit );
}
private void doRematch()
{
Intent intent = GamesList.makeRematchIntent( this, m_gi, m_rowid );
startActivity( intent );
finish();
}
} // class BoardActivity

View file

@ -379,8 +379,13 @@ public class BoardView extends View implements DrawCtx, BoardHandler,
heightLeft = cellSize * 3 / 2;
}
heightLeft /= 3;
trayHt += heightLeft * 2;
scoreHt += heightLeft;
trayHt += heightLeft * 2;
if ( XWPrefs.getSquareTiles( m_context )
&& trayHt > (width / 7) ) {
trayHt = width / 7;
}
heightUsed = trayHt + scoreHt + ((nCells - nToScroll) * cellSize);
}

View file

@ -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"

View file

@ -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<DBChangeListener> s_listeners =
new HashSet<DBChangeListener>();
@ -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<DBChangeListener> iter = s_listeners.iterator();
while ( iter.hasNext() ) {
iter.next().gameSaved( rowid );
iter.next().gameSaved( rowid, countChanged );
}
}
}

View file

@ -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;
}
}
}

View file

@ -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 );

View file

@ -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();
}
}

View file

@ -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();

View file

@ -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<Long,ViewInfo> 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<Void, Void, Void> {
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<Long,ViewInfo>();
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<String,GameGroupInfo> 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;
}
}
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;
}
}

View file

@ -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<Long> s_invalRows = new HashSet<Long>();
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<Void, Void, GameSummary> {
@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<Long> iter = s_invalRows.iterator();
// for ( int ii = 0; iter.hasNext(); ++ii ) {
// strs[ii] = String.format("%d", iter.next() );
// }
// }
// return TextUtils.join(",", strs );
// }
}

View file

@ -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<Long, GameLock>
s_locks = new HashMap<Long,GameLock>();
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;
}
}

View file

@ -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<Long, GameLock>
s_locks = new HashMap<Long,GameLock>();
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();

View file

@ -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 );

View file

@ -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;

View file

@ -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;

View file

@ -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 );

View file

@ -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;
}
}

View file

@ -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 );
}
}

View file

@ -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

View file

@ -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<String> idsWMsgs =
new ArrayList<String>( 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 );
}

View file

@ -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 )
{

View file

@ -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 ) {

View file

@ -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 ) {

View file

@ -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 )
{

View file

@ -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";

View file

@ -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 )
{

View file

@ -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 ) {}
}

View file

@ -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 =

View file

@ -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;

View file

@ -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 <<<EOF
<div class="center">
<p>This page is meant to be viewed on an Android device.</p>
<hr>
<p>(If you <em>are</em> viewing this on an Android device, you&apos;ve
found a bug! Please <a href="mailto:
xwords@eehouse.org?subject=$subject&body=$body">email me</a> (and be
sure to leave the user agent string in the email body.)
<p>(If you <em>are</em> viewing this on an Android device,
you&apos;ve found a bug! Please <a href="mailto:
xwords@eehouse.org?subject=$subject&body=$body">email me</a>
(and be sure to leave the user agent string in the email body.)
</p>
</div>
@ -73,6 +74,11 @@ invite email (or text) and tap the link again.</p>
link in your invite email (or text) again, and this time let
Crosswords handle it.</p>
<p>(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.)</p>
<p>Have fun. And as always, <a href="mailto:xwords@eehouse.org">let
me know</a> if you have problems or suggestions.</p>
</div>

View file

@ -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 );

View file

@ -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();

View file

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