Merge branch 'android_branch' into fix_dlgdelegate

This commit is contained in:
Eric House 2012-12-21 23:03:22 -08:00
commit 110df3c1ba
86 changed files with 3293 additions and 2145 deletions

1
.gitignore vendored
View file

@ -5,3 +5,4 @@ TAGS
core
*.apk
xwords_4.4.0.0*
gcm_loop.shelf

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="46"
android:versionCode="50"
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" />
@ -115,15 +115,31 @@
</intent-filter>
</receiver>
<activity android:name="DispatchNotify"
>
<activity android:name="DispatchNotify">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="newxwgame"/>
</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:scheme="http"
android:host="@string/invite_host"
android:pathPrefix="@string/invite_prefix"
/>
</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

@ -61,6 +61,7 @@
</exec>
<exec dir="." executable="../scripts/gen_gcmid.sh"
output="src/org/eehouse/android/xw4/GCMConsts.java"
logError="true"
/>
<exec dir=".." executable="./scripts/genvers.sh" output="ant_out.txt">
<arg value="XWords4"/>

View file

@ -552,6 +552,7 @@ static const XP_UCHAR*
and_util_getDevID( XW_UtilCtxt* uc, DevIDType* typ )
{
const XP_UCHAR* result = NULL;
*typ = ID_TYPE_NONE;
UTIL_CBK_HEADER( "getDevID", "([B)Ljava/lang/String;" );
jbyteArray jbarr = makeByteArray( env, 1, NULL );
jstring jresult = (*env)->CallObjectMethod( env, util->jutil, mid, jbarr );
@ -581,11 +582,12 @@ and_util_getDevID( XW_UtilCtxt* uc, DevIDType* typ )
}
static void
and_util_deviceRegistered( XW_UtilCtxt* uc, const XP_UCHAR* idRelay )
and_util_deviceRegistered( XW_UtilCtxt* uc, DevIDType typ,
const XP_UCHAR* idRelay )
{
UTIL_CBK_HEADER( "deviceRegistered", "(Ljava/lang/String;)V" );
UTIL_CBK_HEADER( "deviceRegistered", "(ILjava/lang/String;)V" );
jstring jstr = (*env)->NewStringUTF( env, idRelay );
(*env)->CallVoidMethod( env, util->jutil, mid, jstr );
(*env)->CallVoidMethod( env, util->jutil, mid, typ, jstr );
deleteLocalRef( env, jstr );
UTIL_CBK_TAIL();
}

View file

@ -974,9 +974,7 @@ and_send_on_close( XWStreamCtxt* stream, void* closure )
JNIState* state = (JNIState*)globals->state;
XP_ASSERT( !!state->game.comms );
if ( stream_getSize( stream ) > 0 ) {
comms_send( state->game.comms, stream );
}
comms_send( state->game.comms, stream );
}
JNIEXPORT void JNICALL
@ -1307,14 +1305,16 @@ Java_org_eehouse_android_xw4_jni_XwJNI_game_1changeDict
JNIEXPORT void JNICALL
Java_org_eehouse_android_xw4_jni_XwJNI_comms_1resendAll
( JNIEnv* env, jclass C, jint gamePtr, jboolean thenAck )
( JNIEnv* env, jclass C, jint gamePtr, jboolean force, jboolean thenAck )
{
XWJNI_START();
CommsCtxt* comms = state->game.comms;
XP_ASSERT( !!comms );
(void)comms_resendAll( comms );
(void)comms_resendAll( comms, force );
if ( thenAck ) {
#ifdef XWFEATURE_COMMSACK
comms_ackAny( comms );
#endif
}
XWJNI_END();
}

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,12 +5,17 @@
</style>
</head>
<body>
<b>Crosswords 4.4 beta 54 release</b>
<b>Crosswords 4.4 beta 58 release</b>
<h3>New with this release</h3>
<ul>
<li>Allow grouping of games in collapsible user-defined categores:
"Games with Kati", "Finished games", etc.</li>
</ul>
<li>Don't try to access directory OS says is for downloads when it
doesn't actually exist</li>
<h3>Next up</h3>
<ul>
<li>Improve communication with relay</li>
</ul>
<p>(The full changelog

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_version">4.4 beta 54</string>
<string name="app_version">4.4 beta 58</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>
@ -30,7 +31,6 @@
<string name="key_clr_bonushint">key_clr_bonushint</string>
<string name="key_relay_host">key_relay_host</string>
<string name="key_redir_host">key_redir_host</string>
<string name="key_relay_port">key_relay_port2</string>
<string name="key_update_url">key_update_url</string>
<string name="key_update_prerel">key_update_prerel</string>
@ -68,9 +68,10 @@
<string name="key_sms_phones">key_sms_phones</string>
<string name="key_connstat_data">key_connstat_data</string>
<string name="key_dev_id">key_dev_id</string>
<string name="key_gcm_regid">key_gcm_regid</string>
<string name="key_gcmvers_regid">key_gcmvers_regid</string>
<string name="key_relay_regid">key_relay_regid</string>
<string name="key_checked_sms">key_checked_sms</string>
<string name="key_default_group">key_default_group</string>
<string name="key_notagain_sync">key_notagain_sync</string>
<string name="key_notagain_chat">key_notagain_chat</string>
@ -102,9 +103,12 @@
<!-- other -->
<string name="default_host">eehouse.org</string>
<!-- <string name="default_host">10.0.2.2</string> -->
<string name="invite_host">eehouse.org</string>
<string name="invite_prefix">/and/</string>
<string name="invite_mime">application/x-xwordsinvite</string>
<!--string name="invite_mime">text/plain</string-->
<string name="dict_url">http://eehouse.org/and_wordlists</string>
<string name="game_url_pathf">//%1$s/newgame.php</string>
<string name="expl_update_url">Update checks URL</string>
<string name="default_update_url">http://eehouse.org/xw4/info.py</string>

View file

@ -1229,18 +1229,20 @@
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 (%1$s) to accept my invitation and join this
game.\u003cbr\u003E \u003ca
href=\"http://eehouse.org/market_redir.php\"\u003E Tap
here\u003c/a\u003E (http://eehouse.org/market_redir.php) to
install Crosswords if you haven\'t already.</string>
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)
</string>
<!-- This is the body of the text version of the invitation. A URL
is created with parameters describing the game and
substituted for "%1$s".-->
<string name="invite_txtf">Play Crosswords? Join this game: %1$s
. (But install Crosswords http://eehouse.org/market_redir.php
first if you haven\'t.)</string>
<string name="invite_txtf">Let\'s play Crosswords! Join this game:
%1$s .</string>
<!-- When I've created the invitation, in text or html, I ask
Android to launch an app that can send it, typically an email
@ -1427,9 +1429,7 @@
Guest wordlists; Host wins.</string>
<string name="downloading_dictf">Downloading Crosswords
wordlist %s...</string>
<string name="downloading_dictf">Downloading %s...</string>
<!--
############################################################
@ -1461,9 +1461,10 @@
downloading and not opening the game. This first message
takes wordlist name and language substituted in for %1$ and
%2$ -->
<string name="no_dictf">Unable to open game \"%1$s\" because no
%2$s wordlist found. (It may have been deleted, or stored on
an external card that is no longer available.)</string>
<string name="no_dictf">You need to download a replacement %2$s
wordlist before you can open game \"%1$s\". (The original may have
been deleted or stored on an external card that is no longer
available.)</string>
<!-- This is an alternative message presented when there's also
the option of downloading another wordlist. Game name,
@ -1551,8 +1552,8 @@
the same room name over and over so they'll get this warning
and it's harmless to ignore it. -->
<string name="dup_game_queryf">You already have a game that seems
to have been created from the same invitation. Are you sure you
want to open another?</string>
to have been created (on %1$s) from the same invitation. Are you
sure you want to create another?</string>
<!-- Title of generic dialog used to display information -->
<string name="info_title">FYI...</string>
@ -2114,6 +2115,10 @@
play Crosswords using the wordlist %2$s (for play in %3$s), but it
is not installed. Would you like to download the wordlist or
decline the invitation?</string>
<string name="invite_dict_missing_body_nonamef">You have been
invited to play Crosswords using the wordlist %2$s (for play in
%3$s), but it is not installed. Would you like to download the
wordlist?</string>
<string name="button_decline">Decline</string>
<string name="downloadingf">Downloading %s...</string>
@ -2124,4 +2129,16 @@
<string name="default_loc_summary">(Not in external/sdcard memory)</string>
<string name="download_path_title">Downloads Directory</string>
<string name="group_cur_games">My games</string>
<string name="group_new_games">New games</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"
@ -299,6 +304,11 @@
android:summary="Menuitems etc."
android:defaultValue="false"
/>
<!-- For broken devices like my Blaze 4G that report a download
directory that doesn't exist, allow users to set it. Mine:
/sdcard/external_sd/download
-->
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_download_path"
android:title="@string/download_path_title"
@ -324,11 +334,6 @@
android:defaultValue="10998"
android:numeric="decimal"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_redir_host"
android:title="@string/redir_host"
android:defaultValue="@string/default_host"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_dict_host"

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 ) {
@ -444,7 +444,7 @@ public class BTService extends Service {
result = BTCmd.INVITE_ACCPT;
String body = Utils.format( BTService.this,
R.string.new_bt_bodyf, sender );
postNotification( gameID, R.string.new_bt_title, body );
postNotification( gameID, R.string.new_bt_title, body, rowid );
}
} else {
result = BTCmd.INVITE_DUPID;
@ -497,7 +497,7 @@ public class BTService extends Service {
buffer, addr,
m_btMsgSink ) ) {
postNotification( gameID, R.string.new_btmove_title,
R.string.new_move_body );
R.string.new_move_body, rowid );
// do nothing
} else {
DbgUtils.logf( "nobody took msg for gameID %X",
@ -967,17 +967,17 @@ public class BTService extends Service {
return dos;
}
private void postNotification( int gameID, int title, int body )
private void postNotification( int gameID, int title, int body, long rowid )
{
postNotification( gameID, title, getString( body ) );
postNotification( gameID, title, getString( body ), rowid );
}
private void postNotification( int gameID, int title, String body )
private void postNotification( int gameID, int title, String body,
long rowid )
{
Intent intent = new Intent( this, DispatchNotify.class );
intent.putExtra( DispatchNotify.GAMEID_EXTRA, gameID );
Intent intent = GamesList.makeGameIDIntent( this, gameID );
Utils.postNotification( this, intent, R.string.new_btmove_title,
body, gameID );
body, (int)rowid );
}
private Thread killSocketIn( final BluetoothSocket socket )

View file

@ -57,7 +57,7 @@ import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
public class BoardActivity extends XWActivity
implements TransportProcs.TPMsgHandler, View.OnClickListener,
NetUtils.DownloadFinishedListener {
DictImportActivity.DownloadFinishedListener {
public static final String INTENT_KEY_CHAT = "chat";
@ -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,9 +165,10 @@ 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 Object s_thisLocker = new Object();
private static Class s_thisLocker = BoardActivity.class;
public static boolean feedMessage( int gameID, byte[] msg,
CommsAddrRec retAddr )
@ -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 );
@ -259,10 +290,11 @@ public class BoardActivity extends XWActivity
if ( DLG_USEDICT == id ) {
setGotGameDict( m_getDict );
} else {
NetUtils.downloadDictInBack( BoardActivity.this,
m_gi.dictLang,
m_getDict,
BoardActivity.this );
DictImportActivity
.downloadDictInBack( BoardActivity.this,
m_gi.dictLang,
m_getDict,
BoardActivity.this );
}
}
};
@ -498,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();
@ -690,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 )
{
@ -777,7 +816,7 @@ public class BoardActivity extends XWActivity
break;
case R.id.board_menu_game_resend:
m_jniThread.handle( JNICmd.CMD_RESEND, false );
m_jniThread.handle( JNICmd.CMD_RESEND, true, false );
break;
case R.id.gamel_menu_checkmoves:
@ -816,9 +855,8 @@ public class BoardActivity extends XWActivity
if ( DlgDelegate.DISMISS_BUTTON != which ) {
GameUtils.launchInviteActivity( BoardActivity.this,
DlgDelegate.EMAIL_BTN == which,
m_room, null,
m_gi.dictLang,
m_gi.nPlayers );
m_room, null, m_gi.dictLang,
m_gi.dictName, m_gi.nPlayers );
}
} else if ( AlertDialog.BUTTON_POSITIVE == which ) {
JNICmd cmd = JNICmd.CMD_NONE;
@ -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")
@ -1052,7 +1090,7 @@ public class BoardActivity extends XWActivity
}
//////////////////////////////////////////////////
// NetUtils.DownloadFinishedListener interface
// DictImportActivity.DownloadFinishedListener interface
//////////////////////////////////////////////////
public void downloadFinished( final String name, final boolean success )
{
@ -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,11 +1731,17 @@ 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;
}
}
};
m_jniThread = new JNIThread( m_jniGamePtr, m_gi, m_view,
m_gameLock, this, handler );
m_jniThread =
new JNIThread( m_jniGamePtr, stream, m_gi,
m_view, m_gameLock, this, handler );
// see http://stackoverflow.com/questions/680180/where-to-stop-\
// destroy-threads-in-android-service-class
m_jniThread.setDaemon( true );
@ -1722,8 +1766,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 );
@ -1732,7 +1786,7 @@ public class BoardActivity extends XWActivity
if ( null != m_xport ) {
warnIfNoTransport();
trySendChats();
removeNotifications();
Utils.cancelNotification( this, (int)m_rowid );
m_xport.tickle( m_connType );
tryInvites();
}
@ -1923,26 +1977,6 @@ public class BoardActivity extends XWActivity
}
}
private void removeNotifications()
{
int id = 0;
switch( m_connType ) {
case COMMS_CONN_BT:
case COMMS_CONN_SMS:
id = m_gi.gameID;
break;
case COMMS_CONN_RELAY:
String relayID = DBUtils.getRelayID( this, m_rowid );
if ( null != relayID ) {
id = relayID.hashCode();
}
break;
}
if ( 0 != id ) {
Utils.cancelNotification( this, id );
}
}
private void tryInvites()
{
if ( XWApp.BTSUPPORTED || XWApp.SMSSUPPORTED ) {
@ -2094,4 +2128,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

@ -233,23 +233,21 @@ public class CommsTransport implements TransportProcs,
public void tickle( CommsConnType connType )
{
m_jniThread.handle( JNIThread.JNICmd.CMD_RESEND, true );
// CommsAddrRec addr = new CommsAddrRec( m_context );
// XwJNI.comms_getAddr( m_jniGamePtr, addr );
// switch( addr.conType ) {
// case COMMS_CONN_RELAY:
// // do nothing
// break;
// case COMMS_CONN_BT:
// // Let other know I'm here
// m_jniThread.handle( JNIThread.JNICmd.CMD_RESEND );
// break;
// case COMMS_CONN_SMS:
// default:
// DbgUtils.logf( "tickle: unexpected type %s",
// addr.conType.toString() );
// Assert.fail();
// }
switch( connType ) {
case COMMS_CONN_RELAY:
// do nothing
// break; // Try skipping the resend -- later
case COMMS_CONN_BT:
case COMMS_CONN_SMS:
// Let other know I'm here
DbgUtils.logf( "tickle calling comms_resendAll" );
m_jniThread.handle( JNIThread.JNICmd.CMD_RESEND, false, true );
break;
default:
DbgUtils.logf( "tickle: unexpected type %s",
connType.toString() );
Assert.fail();
}
}
private synchronized void putOut( final byte[] buf )

View file

@ -126,7 +126,7 @@ public class ConnStatusHandler {
private static HashMap<CommsConnType,SuccessRecord[]> s_records =
new HashMap<CommsConnType,SuccessRecord[]>();
private static Object s_lockObj = new Object();
private static Class s_lockObj = ConnStatusHandler.class;
private static boolean s_needsSave = false;
public static void setRect( int left, int top, int right, int bottom )
@ -333,8 +333,8 @@ public class ConnStatusHandler {
try {
ObjectInputStream ois =
new ObjectInputStream( new ByteArrayInputStream(bytes) );
Object obj = ois.readObject();
s_records = (HashMap<CommsConnType,SuccessRecord[]>)obj;
s_records =
(HashMap<CommsConnType,SuccessRecord[]>)ois.readObject();
// } catch ( java.io.StreamCorruptedException sce ) {
// DbgUtils.logf( "loadState: %s", sce.toString() );
// } catch ( java.io.OptionalDataException ode ) {

View file

@ -20,9 +20,10 @@
package org.eehouse.android.xw4;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DBHelper extends SQLiteOpenHelper {
@ -30,8 +31,9 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String TABLE_NAME_OBITS = "obits";
public static final String TABLE_NAME_DICTBROWSE = "dictbrowse";
public static final String TABLE_NAME_DICTINFO = "dictinfo";
public static final String TABLE_NAME_GROUPS = "groups";
private static final String DB_NAME = "xwdb";
private static final int DB_VERSION = 14;
private static final int DB_VERSION = 15;
public static final String GAME_NAME = "GAME_NAME";
public static final String NUM_MOVES = "NUM_MOVES";
@ -45,9 +47,6 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String IN_USE = "IN_USE";
public static final String SCORES = "SCORES";
public static final String CHAT_HISTORY = "CHAT_HISTORY";
// GAMEID: this isn't used yet but we'll want it to look up games
// for which messages arrive. Add now while changing the DB
// format
public static final String GAMEID = "GAMEID";
public static final String REMOTEDEVS = "REMOTEDEVS";
public static final String DICTLANG = "DICTLANG";
@ -61,9 +60,9 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String INVITEID = "INVITEID";
public static final String RELAYID = "RELAYID";
public static final String SEED = "SEED";
public static final String SMSPHONE = "SMSPHONE";
public static final String SMSPHONE = "SMSPHONE"; // unused -- so far
public static final String LASTMOVE = "LASTMOVE";
public static final String GROUPID = "GROUPID";
public static final String DICTNAME = "DICTNAME";
public static final String MD5SUM = "MD5SUM";
@ -76,16 +75,79 @@ public class DBHelper extends SQLiteOpenHelper {
public static final String ITERPOS = "ITERPOS";
public static final String ITERTOP = "ITERTOP";
public static final String ITERPREFIX = "ITERPREFIX";
// not used yet
public static final String CREATE_TIME = "CREATE_TIME";
// not used yet
public static final String LASTPLAY_TIME = "LASTPLAY_TIME";
public static final String GROUPNAME = "GROUPNAME";
public static final String EXPANDED = "EXPANDED";
private Context m_context;
private static final String[] s_summaryColsAndTypes = {
GAME_NAME, "TEXT"
,NUM_MOVES, "INTEGER"
,TURN, "INTEGER"
,GIFLAGS, "INTEGER"
,NUM_PLAYERS, "INTEGER"
,MISSINGPLYRS,"INTEGER"
,PLAYERS, "TEXT"
,GAME_OVER, "INTEGER"
,SERVERROLE, "INTEGER"
,CONTYPE, "INTEGER"
,ROOMNAME, "TEXT"
,INVITEID, "TEXT"
,RELAYID, "TEXT"
,SEED, "INTEGER"
,DICTLANG, "INTEGER"
,DICTLIST, "TEXT"
,SMSPHONE, "TEXT" // unused
,SCORES, "TEXT"
,CHAT_HISTORY, "TEXT"
,GAMEID, "INTEGER"
,REMOTEDEVS, "TEXT"
,LASTMOVE, "INTEGER DEFAULT 0"
,GROUPID, "INTEGER"
// HASMSGS: sqlite doesn't have bool; use 0 and 1
,HASMSGS, "INTEGER DEFAULT 0"
,CONTRACTED, "INTEGER DEFAULT 0"
,CREATE_TIME, "INTEGER"
,LASTPLAY_TIME,"INTEGER"
,SNAPSHOT, "BLOB"
};
private static final String[] s_obitsColsAndTypes = {
RELAYID, "TEXT"
,SEED, "INTEGER"
};
private static final String[] s_dictInfoColsAndTypes = {
DICTNAME, "TEXT"
,LOC, "UNSIGNED INTEGER(1)"
,MD5SUM, "TEXT(32)"
,WORDCOUNT,"INTEGER"
,LANGCODE, "INTEGER"
};
private static final String[] s_dictBrowseColsAndTypes = {
DICTNAME, "TEXT"
,LOC, "UNSIGNED INTEGER(1)"
,WORDCOUNTS, "TEXT"
,ITERMIN, "INTEGRE(4)"
,ITERMAX, "INTEGER(4)"
,ITERPOS, "INTEGER"
,ITERTOP, "INTEGER"
,ITERPREFIX, "TEXT"
};
private static final String[] s_groupsSchema = {
GROUPNAME, "TEXT"
,EXPANDED, "INTEGER(1)"
};
public DBHelper( Context context )
{
super( context, DB_NAME, null, DB_VERSION );
m_context = context;
}
public static String getDBName()
@ -93,81 +155,14 @@ public class DBHelper extends SQLiteOpenHelper {
return DB_NAME;
}
private void onCreateSum( SQLiteDatabase db )
{
db.execSQL( "CREATE TABLE " + TABLE_NAME_SUM + " ("
+ GAME_NAME + " TEXT,"
+ NUM_MOVES + " INTEGER,"
+ TURN + " INTEGER,"
+ GIFLAGS + " INTEGER,"
+ NUM_PLAYERS + " INTEGER,"
+ MISSINGPLYRS + " INTEGER,"
+ PLAYERS + " TEXT,"
+ GAME_OVER + " INTEGER,"
+ SERVERROLE + " INTEGER,"
+ CONTYPE + " INTEGER,"
+ ROOMNAME + " TEXT,"
+ INVITEID + " TEXT,"
+ RELAYID + " TEXT,"
+ SEED + " INTEGER,"
+ DICTLANG + " INTEGER,"
+ DICTLIST + " TEXT,"
+ SMSPHONE + " TEXT,"
+ SCORES + " TEXT,"
+ CHAT_HISTORY + " TEXT,"
+ GAMEID + " INTEGER,"
+ REMOTEDEVS + " TEXT,"
+ LASTMOVE + " INTEGER DEFAULT 0,"
// HASMSGS: sqlite doesn't have bool; use 0 and 1
+ HASMSGS + " INTEGER DEFAULT 0,"
+ CONTRACTED + " INTEGER DEFAULT 0,"
+ CREATE_TIME + " INTEGER,"
+ LASTPLAY_TIME + " INTEGER,"
+ SNAPSHOT + " BLOB);"
);
}
private void onCreateObits( SQLiteDatabase db )
{
db.execSQL( "CREATE TABLE " + TABLE_NAME_OBITS + " ("
+ RELAYID + " TEXT,"
+ SEED + " INTEGER);"
);
}
private void onCreateDictsDB( SQLiteDatabase db )
{
db.execSQL( "CREATE TABLE " + TABLE_NAME_DICTINFO + "("
+ DICTNAME + " TEXT,"
+ LOC + " UNSIGNED INTEGER(1),"
+ MD5SUM + " TEXT(32),"
+ WORDCOUNT + " INTEGER,"
+ LANGCODE + " INTEGER);"
);
db.execSQL( "CREATE TABLE " + TABLE_NAME_DICTBROWSE + "("
+ DICTNAME + " TEXT,"
+ LOC + " UNSIGNED INTEGER(1),"
+ WORDCOUNTS + " TEXT,"
+ ITERMIN + " INTEGER(4),"
+ ITERMAX + " INTEGER(4),"
+ ITERPOS + " INTEGER,"
+ ITERTOP + " INTEGER,"
+ ITERPREFIX + " TEXT);"
);
}
@Override
public void onCreate( SQLiteDatabase db )
{
onCreateSum( db );
onCreateObits( db );
onCreateDictsDB( db );
createTable( db, TABLE_NAME_SUM, s_summaryColsAndTypes );
createTable( db, TABLE_NAME_OBITS, s_obitsColsAndTypes );
createTable( db, TABLE_NAME_DICTINFO, s_dictInfoColsAndTypes );
createTable( db, TABLE_NAME_DICTBROWSE, s_dictBrowseColsAndTypes );
createGroupsTable( db );
}
@Override
@ -178,26 +173,30 @@ public class DBHelper extends SQLiteOpenHelper {
switch( oldVersion ) {
case 5:
onCreateObits(db);
createTable( db, TABLE_NAME_OBITS, s_obitsColsAndTypes );
case 6:
addColumn( db, TURN, "INTEGER" );
addColumn( db, GIFLAGS, "INTEGER" );
addColumn( db, CHAT_HISTORY, "TEXT" );
addSumColumn( db, TURN );
addSumColumn( db, GIFLAGS );
addSumColumn( db, CHAT_HISTORY );
case 7:
addColumn( db, MISSINGPLYRS, "INTEGER" );
addSumColumn( db, MISSINGPLYRS );
case 8:
addColumn( db, GAME_NAME, "TEXT" );
addColumn( db, CONTRACTED, "INTEGER" );
addSumColumn( db, GAME_NAME );
addSumColumn( db, CONTRACTED );
case 9:
addColumn( db, DICTLIST, "TEXT" );
addSumColumn( db, DICTLIST );
case 10:
addColumn( db, INVITEID, "TEXT" );
addSumColumn( db, INVITEID );
case 11:
addColumn( db, REMOTEDEVS, "TEXT" );
addSumColumn( db, REMOTEDEVS );
case 12:
onCreateDictsDB( db );
createTable( db, TABLE_NAME_DICTINFO, s_dictInfoColsAndTypes );
createTable( db, TABLE_NAME_DICTBROWSE, s_dictBrowseColsAndTypes );
case 13:
addColumn( db, LASTMOVE, "INTEGER" );
addSumColumn( db, LASTMOVE );
case 14:
addSumColumn( db, GROUPID );
createGroupsTable( db );
// nothing yet
break;
default:
@ -209,10 +208,56 @@ public class DBHelper extends SQLiteOpenHelper {
}
}
private void addColumn( SQLiteDatabase db, String colName, String colType )
private void addSumColumn( SQLiteDatabase db, String colName )
{
String colType = null;
for ( int ii = 0; ii < s_summaryColsAndTypes.length; ii += 2 ) {
if ( s_summaryColsAndTypes[ii].equals( colName ) ) {
colType = s_summaryColsAndTypes[ii+1];
break;
}
}
String cmd = String.format( "ALTER TABLE %s ADD COLUMN %s %s;",
TABLE_NAME_SUM, colName, colType );
db.execSQL( cmd );
}
private void createTable( SQLiteDatabase db, String name, String[] data )
{
StringBuilder query =
new StringBuilder( String.format("CREATE TABLE %s (", name ) );
for ( int ii = 0; ii < data.length; ii += 2 ) {
String col = String.format( " %s %s,", data[ii], data[ii+1] );
query.append( col );
}
query.setLength(query.length() - 1); // nuke the last comma
query.append( ");" );
db.execSQL( query.toString() );
}
private void createGroupsTable( SQLiteDatabase db )
{
createTable( db, TABLE_NAME_GROUPS, s_groupsSchema );
// Create an empty group name
ContentValues values = new ContentValues();
values.put( GROUPNAME, m_context.getString(R.string.group_cur_games) );
values.put( EXPANDED, 1 );
long curGroup = db.insert( TABLE_NAME_GROUPS, null, values );
values = new ContentValues();
values.put( GROUPNAME, m_context.getString(R.string.group_new_games) );
values.put( EXPANDED, 0 );
long newGroup = db.insert( TABLE_NAME_GROUPS, null, values );
// place all existing games in the initial unnamed group
values = new ContentValues();
values.put( GROUPID, curGroup );
db.update( DBHelper.TABLE_NAME_SUM, values, null, null );
XWPrefs.setDefaultNewGameGroup( m_context, newGroup );
}
}

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
@ -364,24 +367,15 @@ public class DBUtils {
private static void setInt( long rowid, String column, int value )
{
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getWritableDatabase();
String selection = String.format( ROW_ID_FMT, rowid );
ContentValues values = new ContentValues();
values.put( column, value );
int result = db.update( DBHelper.TABLE_NAME_SUM,
values, selection, null );
Assert.assertTrue( result == 1 );
db.close();
}
ContentValues values = new ContentValues();
values.put( column, value );
updateRow( null, DBHelper.TABLE_NAME_SUM, rowid, values );
}
public static void setMsgFlags( long rowid, int flags )
{
setInt( rowid, DBHelper.HASMSGS, flags );
notifyListeners( rowid );
notifyListeners( rowid, false );
}
public static void setExpanded( long rowid, boolean expanded )
@ -456,10 +450,8 @@ public class DBUtils {
String selection = DBHelper.RELAYID + "='" + relayID + "'";
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, null, null, null, null );
result = new long[cursor.getCount()];
for ( int ii = 0; cursor.moveToNext(); ++ii ) {
if ( null == result ) {
result = new long[cursor.getCount()];
}
result[ii] = cursor.getLong( cursor.getColumnIndex(ROW_ID) );
}
cursor.close();
@ -478,11 +470,8 @@ public class DBUtils {
String selection = String.format( DBHelper.GAMEID + "=%d", gameID );
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, null, null, null, null );
result = new long[cursor.getCount()];
for ( int ii = 0; cursor.moveToNext(); ++ii ) {
if ( null == result ) {
result = new long[cursor.getCount()];
}
result[ii] = cursor.getLong( cursor.getColumnIndex(ROW_ID) );
}
cursor.close();
@ -544,21 +533,28 @@ public class DBUtils {
}
}
public static long getRowIDForOpen( Context context, NetLaunchInfo nli )
// Return creation time of newest game matching this nli, or null
// if none found.
public static Date getMostRecentCreate( Context context,
NetLaunchInfo nli )
{
long result = ROWID_NOTFOUND;
Date result = null;
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getReadableDatabase();
String[] columns = { ROW_ID };
String selection = DBHelper.ROOMNAME + "='" + nli.room + "' AND "
+ DBHelper.INVITEID + "='" + nli.inviteID + "' AND "
+ DBHelper.DICTLANG + "=" + nli.lang + " AND "
+ DBHelper.NUM_PLAYERS + "=" + nli.nPlayers;
String[] columns = { DBHelper.CREATE_TIME };
String selection =
String.format( "%s='%s' AND %s='%s' AND %s=%d AND %s=%d",
DBHelper.ROOMNAME, nli.room,
DBHelper.INVITEID, nli.inviteID,
DBHelper.DICTLANG, nli.lang,
DBHelper.NUM_PLAYERS, nli.nPlayersT );
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, null, null, null, null );
if ( 1 == cursor.getCount() && cursor.moveToFirst() ) {
result = cursor.getLong( cursor.getColumnIndex(ROW_ID) );
selection, null, null, null,
DBHelper.CREATE_TIME + " DESC" ); // order by
if ( cursor.moveToNext() ) {
int indx = cursor.getColumnIndex( DBHelper.CREATE_TIME );
result = new Date( cursor.getLong( indx ) );
}
cursor.close();
db.close();
@ -566,13 +562,17 @@ public class DBUtils {
return result;
}
public static boolean isNewInvite( Context context, Uri data )
public static Date getMostRecentCreate( Context context, Uri data )
{
NetLaunchInfo nli = new NetLaunchInfo( data );
return null != nli && -1 == getRowIDForOpen( context, nli );
Date result = null;
NetLaunchInfo nli = new NetLaunchInfo( context, data );
if ( null != nli && nli.isValid() ) {
result = getMostRecentCreate( context, nli );
}
return result;
}
public static String[] getRelayIDs( Context context, boolean noMsgs )
public static String[] getRelayIDs( Context context, long[][] rowIDs )
{
String[] result = null;
initDB( context );
@ -580,26 +580,31 @@ public class DBUtils {
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getReadableDatabase();
String[] columns = { DBHelper.RELAYID };
String[] columns = { ROW_ID, DBHelper.RELAYID };
String selection = DBHelper.RELAYID + " NOT null";
if ( noMsgs ) {
selection += " AND NOT " + DBHelper.HASMSGS;
}
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
selection, null, null, null, null );
int count = cursor.getCount();
if ( 0 < count ) {
result = new String[count];
if ( null != rowIDs ) {
rowIDs[0] = new long[count];
}
while ( cursor.moveToNext() ) {
ids.add( cursor.getString( cursor.
getColumnIndex(DBHelper.RELAYID)) );
int idIndex = cursor.getColumnIndex(DBHelper.RELAYID);
int rowIndex = cursor.getColumnIndex(ROW_ID);
for ( int ii = 0; cursor.moveToNext(); ++ii ) {
result[ii] = cursor.getString( idIndex );
if ( null != rowIDs ) {
rowIDs[0][ii] = cursor.getLong( rowIndex );
}
}
}
cursor.close();
db.close();
}
if ( 0 < ids.size() ) {
result = ids.toArray( new String[ids.size()] );
}
return result;
}
@ -673,9 +678,9 @@ public class DBUtils {
}
}
public static GameUtils.GameLock saveNewGame( Context context, byte[] bytes )
public static GameLock saveNewGame( Context context, byte[] bytes )
{
GameUtils.GameLock lock = null;
GameLock lock = null;
initDB( context );
synchronized( s_dbHelper ) {
@ -687,58 +692,46 @@ public class DBUtils {
long timestamp = new Date().getTime();
values.put( DBHelper.CREATE_TIME, timestamp );
values.put( DBHelper.LASTPLAY_TIME, timestamp );
values.put( DBHelper.GROUPID,
XWPrefs.getDefaultNewGameGroup( context ) );
long rowid = db.insert( DBHelper.TABLE_NAME_SUM, null, values );
setCached( rowid, null ); // force reread
clearRowIDsCache();
lock = new GameUtils.GameLock( rowid, true ).lock();
notifyListeners( rowid );
lock = new GameLock( rowid, true ).lock();
notifyListeners( rowid, true );
}
return lock;
}
public static long saveGame( Context context, GameUtils.GameLock lock,
public static long saveGame( Context context, GameLock lock,
byte[] bytes, boolean setCreate )
{
Assert.assertTrue( lock.canWrite() );
long rowid = lock.getRowid();
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getWritableDatabase();
String selection = String.format( ROW_ID_FMT, rowid );
ContentValues values = new ContentValues();
values.put( DBHelper.SNAPSHOT, bytes );
ContentValues values = new ContentValues();
values.put( DBHelper.SNAPSHOT, bytes );
long timestamp = new Date().getTime();
if ( setCreate ) {
values.put( DBHelper.CREATE_TIME, timestamp );
}
values.put( DBHelper.LASTPLAY_TIME, timestamp );
int result = db.update( DBHelper.TABLE_NAME_SUM,
values, selection, null );
if ( 0 == result ) {
Assert.fail();
// values.put( DBHelper.FILE_NAME, path );
// rowid = db.insert( DBHelper.TABLE_NAME_SUM, null, values );
// DbgUtils.logf( "insert=>%d", rowid );
// Assert.assertTrue( row >= 0 );
}
db.close();
long timestamp = new Date().getTime();
if ( setCreate ) {
values.put( DBHelper.CREATE_TIME, timestamp );
}
setCached( rowid, null ); // force reread
values.put( DBHelper.LASTPLAY_TIME, timestamp );
if ( -1 != rowid ) {
notifyListeners( rowid );
updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
setCached( rowid, null ); // force reread
if ( -1 != rowid ) { // Means new game?
notifyListeners( rowid, false );
}
return rowid;
}
public static byte[] loadGame( Context context, GameUtils.GameLock lock )
public static byte[] loadGame( Context context, GameLock lock )
{
long rowid = lock.getRowid();
Assert.assertTrue( -1 != rowid );
@ -766,12 +759,16 @@ public class DBUtils {
public static void deleteGame( Context context, long rowid )
{
GameUtils.GameLock lock = new GameUtils.GameLock( rowid, true ).lock();
deleteGame( context, lock );
lock.unlock();
GameLock lock = new GameLock( rowid, true ).lock( 300 );
if ( null != lock ) {
deleteGame( context, lock );
lock.unlock();
} else {
DbgUtils.logf( "deleteGame: unable to lock rowid %d", rowid );
}
}
public static void deleteGame( Context context, GameUtils.GameLock lock )
public static void deleteGame( Context context, GameLock lock )
{
Assert.assertTrue( lock.canWrite() );
initDB( context );
@ -781,34 +778,46 @@ public class DBUtils {
db.delete( DBHelper.TABLE_NAME_SUM, selection, null );
db.close();
}
notifyListeners( lock.getRowid() );
clearRowIDsCache();
notifyListeners( lock.getRowid(), true );
}
public static long[] gamesList( Context context )
{
long[] result = null;
long[] result;
synchronized( DBUtils.class ) {
if ( null == s_cachedRowIDs ) {
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getReadableDatabase();
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getReadableDatabase();
String[] columns = { ROW_ID };
String orderBy = DBHelper.CREATE_TIME + " DESC";
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM, columns,
null, null, null, null, orderBy );
int count = cursor.getCount();
result = new long[count];
int index = cursor.getColumnIndex( ROW_ID );
for ( int ii = 0; cursor.moveToNext(); ++ii ) {
result[ii] = cursor.getLong( index );
String[] columns = { ROW_ID };
String orderBy = DBHelper.CREATE_TIME + " DESC";
Cursor cursor = db.query( DBHelper.TABLE_NAME_SUM,
columns, null, null, null,
null, orderBy );
int count = cursor.getCount();
s_cachedRowIDs = new long[count];
int index = cursor.getColumnIndex( ROW_ID );
for ( int ii = 0; cursor.moveToNext(); ++ii ) {
s_cachedRowIDs[ii] = cursor.getLong( index );
}
cursor.close();
db.close();
}
}
cursor.close();
db.close();
result = s_cachedRowIDs;
}
return result;
}
private static void clearRowIDsCache()
{
synchronized( DBUtils.class ) {
s_cachedRowIDs = null;
}
}
// Get either the file name or game name, preferring the latter.
public static String getName( Context context, long rowid )
{
@ -834,21 +843,9 @@ public class DBUtils {
public static void setName( Context context, long rowid, String name )
{
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getWritableDatabase();
String selection = String.format( ROW_ID_FMT, rowid );
ContentValues values = new ContentValues();
values.put( DBHelper.GAME_NAME, name );
int result = db.update( DBHelper.TABLE_NAME_SUM,
values, selection, null );
db.close();
if ( 0 == result ) {
DbgUtils.logf( "setName(%d,%s) failed", rowid, name );
}
}
ContentValues values = new ContentValues();
values.put( DBHelper.GAME_NAME, name );
updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
}
public static HistoryPair[] getChatHistory( Context context, long rowid )
@ -928,6 +925,7 @@ public class DBUtils {
public static void loadDB( Context context )
{
clearRowIDsCache();
copyGameDB( context, false );
}
@ -1190,42 +1188,49 @@ public class DBUtils {
private static void saveChatHistory( Context context, long rowid,
String history )
{
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getWritableDatabase();
String selection = String.format( ROW_ID_FMT, rowid );
ContentValues values = new ContentValues();
if ( null != history ) {
values.put( DBHelper.CHAT_HISTORY, history );
} else {
values.putNull( DBHelper.CHAT_HISTORY );
}
long timestamp = new Date().getTime();
values.put( DBHelper.LASTPLAY_TIME, timestamp );
int result = db.update( DBHelper.TABLE_NAME_SUM,
values, selection, null );
db.close();
ContentValues values = new ContentValues();
if ( null != history ) {
values.put( DBHelper.CHAT_HISTORY, history );
} else {
values.putNull( DBHelper.CHAT_HISTORY );
}
values.put( DBHelper.LASTPLAY_TIME, new Date().getTime() );
updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
}
private static void initDB( Context context )
{
if ( null == s_dbHelper ) {
Assert.assertNotNull( context );
s_dbHelper = new DBHelper( context );
// force any upgrade
s_dbHelper.getWritableDatabase().close();
}
}
private static void notifyListeners( long rowid )
private static void updateRow( Context context, String table,
long rowid, ContentValues values )
{
initDB( context );
synchronized( s_dbHelper ) {
SQLiteDatabase db = s_dbHelper.getWritableDatabase();
String selection = String.format( ROW_ID_FMT, rowid );
int result = db.update( table, values, selection, null );
db.close();
if ( 0 == result ) {
DbgUtils.logf( "updateRow failed" );
}
}
}
private static void notifyListeners( long rowid, boolean countChanged )
{
synchronized( s_listeners ) {
Iterator<DBChangeListener> iter = s_listeners.iterator();
while ( iter.hasNext() ) {
iter.next().gameSaved( rowid );
iter.next().gameSaved( rowid, countChanged );
}
}
}

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,31 +21,72 @@
package org.eehouse.android.xw4;
import android.app.Activity;
import android.os.Bundle;
import android.os.AsyncTask;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Window;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.io.InputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URI;
import java.security.MessageDigest;
import java.util.HashMap;
import junit.framework.Assert;
public class DictImportActivity extends XWActivity {
// URIs coming in in intents
private static final String APK_EXTRA = "APK";
private static final String DICT_EXTRA = "XWD";
public interface DownloadFinishedListener {
void downloadFinished( String name, boolean success );
}
// Track callbacks for downloads.
private static class ListenerData {
public ListenerData( String dictName, DownloadFinishedListener lstnr )
{
m_dictName = dictName;
m_lstnr = lstnr;
}
public String m_dictName;
public DownloadFinishedListener m_lstnr;
}
private static HashMap<String,ListenerData> s_listeners =
new HashMap<String,ListenerData>();
private class DownloadFilesTask extends AsyncTask<Uri, Integer, Long> {
private String m_saved = null;
private String m_savedDict = null;
private String m_url = null;
private boolean m_isApp = false;
private File m_appFile = null;
public DownloadFilesTask( boolean isApp )
{
super();
m_isApp = isApp;
}
public DownloadFilesTask( String url, boolean isApp )
{
this( isApp );
m_url = url;
}
@Override
protected Long doInBackground( Uri... uris )
{
m_saved = null;
m_savedDict = null;
m_appFile = null;
int count = uris.length;
Assert.assertTrue( 1 == count );
long totalSize = 0;
for ( int ii = 0; ii < count; ii++ ) {
Uri uri = uris[ii];
DbgUtils.logf( "trying %s", uri );
@ -55,7 +96,12 @@ public class DictImportActivity extends XWActivity {
uri.getSchemeSpecificPart(),
uri.getFragment() );
InputStream is = jUri.toURL().openStream();
m_saved = saveDict( is, uri.getPath() );
String name = basename( uri.getPath() );
if ( m_isApp ) {
m_appFile = saveToDownloads( is, name );
} else {
m_savedDict = saveDict( is, name );
}
is.close();
} catch ( java.net.URISyntaxException use ) {
DbgUtils.loge( use );
@ -65,18 +111,26 @@ public class DictImportActivity extends XWActivity {
DbgUtils.loge( ioe );
}
}
return totalSize;
return new Long(0);
}
@Override
protected void onPostExecute( Long result )
{
DbgUtils.logf( "onPostExecute passed %d", result );
if ( null != m_saved ) {
if ( null != m_savedDict ) {
DictUtils.DictLoc loc =
XWPrefs.getDefaultLoc( DictImportActivity.this );
DictLangCache.inval( DictImportActivity.this, m_saved,
DictLangCache.inval( DictImportActivity.this, m_savedDict,
loc, true );
callListener( m_url, true );
} else if ( null != m_appFile ) {
// launch the installer
Intent intent = Utils.makeInstallIntent( m_appFile );
startActivity( intent );
} else {
// we failed at something....
callListener( m_url, false );
}
finish();
}
@ -86,6 +140,7 @@ public class DictImportActivity extends XWActivity {
protected void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
DownloadFilesTask dft = null;
requestWindowFeature( Window.FEATURE_LEFT_ICON );
setContentView( R.layout.import_dict );
@ -96,27 +151,64 @@ public class DictImportActivity extends XWActivity {
Intent intent = getIntent();
Uri uri = intent.getData();
if ( null != uri) {
if ( null != intent.getType()
&& intent.getType().equals( "application/x-xwordsdict" ) ) {
DbgUtils.logf( "based on MIME type" );
new DownloadFilesTask().execute( uri );
} else if ( uri.toString().endsWith( XWConstants.DICT_EXTN ) ) {
String txt = getString( R.string.downloading_dictf,
basename( uri.getPath()) );
TextView view = (TextView)findViewById( R.id.dwnld_message );
view.setText( txt );
new DownloadFilesTask().execute( uri );
} else {
DbgUtils.logf( "bogus intent: %s/%s", intent.getType(), uri );
finish();
}
if ( null == uri ) {
String url = intent.getStringExtra( APK_EXTRA );
boolean isApp = null != url;
if ( !isApp ) {
url = intent.getStringExtra( DICT_EXTRA );
}
if ( null != url ) {
dft = new DownloadFilesTask( url, isApp );
uri = Uri.parse( url );
}
} else if ( null != intent.getType()
&& intent.getType().equals( "application/x-xwordsdict" ) ) {
dft = new DownloadFilesTask( false );
} else if ( uri.toString().endsWith( XWConstants.DICT_EXTN ) ) {
dft = new DownloadFilesTask( uri.toString(), false );
}
if ( null == dft ) {
finish();
} else {
String showName = basename( uri.getPath() );
String msg = getString( R.string.downloading_dictf, showName );
TextView view = (TextView)findViewById( R.id.dwnld_message );
view.setText( msg );
dft.execute( uri );
}
}
private String saveDict( InputStream inputStream, String path )
private File saveToDownloads( InputStream is, String name )
{
boolean success = false;
File appFile = new File( DictUtils.getDownloadDir( this ), name );
byte[] buf = new byte[1024*4];
try {
FileOutputStream fos = new FileOutputStream( appFile );
int nRead;
while ( 0 <= (nRead = is.read( buf, 0, buf.length )) ) {
fos.write( buf, 0, nRead );
}
fos.close();
success = true;
} catch ( java.io.FileNotFoundException fnf ) {
DbgUtils.loge( fnf );
} catch ( java.io.IOException ioe ) {
DbgUtils.loge( ioe );
}
if ( !success ) {
appFile.delete();
appFile = null;
}
return appFile;
}
private String saveDict( InputStream inputStream, String name )
{
String name = basename( path );
DictUtils.DictLoc loc = XWPrefs.getDefaultLoc( this );
if ( !DictUtils.saveDict( this, inputStream, name, loc ) ) {
name = null;
@ -128,6 +220,57 @@ public class DictImportActivity extends XWActivity {
{
return new File(path).getName();
}
private static void rememberListener( String url, String name,
DownloadFinishedListener lstnr )
{
ListenerData ld = new ListenerData( name, lstnr );
synchronized( s_listeners ) {
s_listeners.put( url, ld );
}
}
private static void callListener( String url, boolean success )
{
if ( null != url ) {
ListenerData ld;
synchronized( s_listeners ) {
ld = s_listeners.get( url );
if ( null != ld ) {
s_listeners.remove( url );
}
}
if ( null != ld ) {
ld.m_lstnr.downloadFinished( ld.m_dictName, success );
}
}
}
public static void downloadDictInBack( Context context, int lang,
String name,
DownloadFinishedListener lstnr )
{
String url = Utils.makeDictUrl( context, lang, name );
if ( null != lstnr ) {
rememberListener( url, name, lstnr );
}
downloadDictInBack( context, url );
}
public static void downloadDictInBack( Context context, String url )
{
Intent intent = new Intent( context, DictImportActivity.class );
intent.putExtra( DICT_EXTRA, url );
context.startActivity( intent );
}
public static Intent makeAppDownloadIntent( Context context, String url )
{
Intent intent = new Intent( context, DictImportActivity.class );
intent.putExtra( APK_EXTRA, url );
return intent;
}
}

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 );
}
private static File getDownloadDir( Context context )
// Loop through three ways of getting the directory until one
// produces a directory I can write to.
public static File getDownloadDir( Context context )
{
File result = null;
if ( haveWriteableSD() ) {
File file = null;
String myPath = XWPrefs.getMyDownloadDir( context );
if ( null != myPath && 0 < myPath.length() ) {
file = new File( myPath );
} else {
file = Environment.getExternalStorageDirectory();
if ( null != file ) {
file = new File( file, "download/" );
outer:
for ( int attempt = 0; attempt < 4; ++attempt ) {
switch ( attempt ) {
case 0:
String myPath = XWPrefs.getMyDownloadDir( context );
if ( null == myPath || 0 == myPath.length() ) {
continue;
}
result = new File( myPath );
break;
case 1:
if ( null == s_dirGetter ) {
continue;
}
result = s_dirGetter.getDownloadDir();
break;
case 2:
case 3:
if ( !haveWriteableSD() ) {
continue;
}
result = Environment.getExternalStorageDirectory();
if ( 2 == attempt && null != result ) {
// the old way...
result = new File( result, "download/" );
}
break;
}
if ( null != file && file.exists() && file.isDirectory() ) {
result = file;
// Exit test for loop
if ( null != result ) {
if ( result.exists() && result.isDirectory() && result.canWrite() ) {
break outer;
}
}
}
return result;
@ -596,4 +633,13 @@ public class DictUtils {
}
return result;
}
private static class DirGetter implements SafeDirGetter {
public File getDownloadDir()
{
File path = Environment.
getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
return path;
}
}
}

View file

@ -61,7 +61,7 @@ import org.eehouse.android.xw4.DictUtils.DictLoc;
public class DictsActivity extends ExpandableListActivity
implements View.OnClickListener, XWListItem.DeleteCallback,
MountEventReceiver.SDCardNotifiee, DlgDelegate.DlgClickNotify,
NetUtils.DownloadFinishedListener {
DictImportActivity.DownloadFinishedListener {
private static final String DICT_DOLAUNCH = "do_launch";
private static final String DICT_LANG_EXTRA = "use_lang";
@ -339,8 +339,9 @@ public class DictsActivity extends ExpandableListActivity
String name = intent.getStringExtra( MultiService.DICT );
m_launchedForMissing = true;
m_handler = new Handler();
NetUtils.downloadDictInBack( DictsActivity.this, lang,
name, DictsActivity.this );
DictImportActivity
.downloadDictInBack( DictsActivity.this, lang,
name, DictsActivity.this );
}
};
lstnr2 = new OnClickListener() {
@ -555,10 +556,9 @@ public class DictsActivity extends ExpandableListActivity
{
int loci = intent.getIntExtra( UpdateCheckReceiver.NEW_DICT_LOC, 0 );
if ( 0 < loci ) {
DictLoc loc = DictLoc.values()[loci];
String url =
intent.getStringExtra( UpdateCheckReceiver.NEW_DICT_URL );
NetUtils.downloadDictInBack( this, url, loc, null );
DictImportActivity.downloadDictInBack( this, url );
finish();
}
}
@ -769,7 +769,7 @@ public class DictsActivity extends ExpandableListActivity
launchAndDownload( activity, 0, null );
}
// NetUtils.DownloadFinishedListener interface
// DictImportActivity.DownloadFinishedListener interface
public void downloadFinished( String name, final boolean success )
{
if ( m_launchedForMissing ) {

View file

@ -29,145 +29,20 @@ import android.os.Bundle;
import java.util.HashSet;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.GameSummary;
public class DispatchNotify extends Activity {
public static final String RELAYIDS_EXTRA = "relayids";
public static final String GAMEID_EXTRA = "gameid";
public interface HandleRelaysIface {
void handleRelaysIDs( final String[] relayIDs );
void handleInvite( final Uri invite );
void handleGameID( int gameID );
}
private static HashSet<HandleRelaysIface> s_running =
new HashSet<HandleRelaysIface>();
private static HandleRelaysIface s_handler;
@Override
protected void onCreate( Bundle savedInstanceState )
{
boolean mustLaunch = false;
super.onCreate( savedInstanceState );
String[] relayIDs = getIntent().getStringArrayExtra( RELAYIDS_EXTRA );
int gameID = getIntent().getIntExtra( GAMEID_EXTRA, -1 );
Uri data = getIntent().getData();
if ( null != relayIDs ) {
if ( !tryHandle( relayIDs ) ) {
mustLaunch = true;
}
} else if ( -1 != gameID ) {
if ( !tryHandle( gameID ) ) {
mustLaunch = true;
}
} else if ( null != data ) {
if ( DBUtils.isNewInvite( this, data ) ) {
if ( !tryHandle( data ) ) {
mustLaunch = true;
}
} else {
DbgUtils.logf( "DispatchNotify: dropping duplicate invite" );
}
}
if ( mustLaunch ) {
DbgUtils.logf( "DispatchNotify: nothing running" );
Intent intent = new Intent( this, GamesList.class );
// This combination of flags will bring an existing
// GamesList instance to the front, killing any children
// it has, or create a new one if none exists. Coupled
// with a "standard" launchMode it seems to work, meaning
// both that the app preserves its stack in normal use
// (you can go to Home with a stack of activities and
// return to the top activity on that stack if you
// relaunch the app) and that when I launch from here the
// stack gets nuked and we don't get a second GamesList
// instance.
intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_NEW_TASK );
if ( null != relayIDs ) {
intent.putExtra( RELAYIDS_EXTRA, relayIDs );
} else if ( -1 != gameID ) {
intent.putExtra( GAMEID_EXTRA, gameID );
} else if ( null != data ) {
intent.setData( data );
} else {
Assert.fail();
}
startActivity( intent );
if ( null != data ) { // relay invite redirected URL case
GamesList.openGame( this, data );
}
finish();
}
public static void SetRunning( Activity running )
{
if ( running instanceof HandleRelaysIface ) {
s_running.add( (HandleRelaysIface)running );
}
}
public static void ClearRunning( Activity running )
{
if ( running instanceof HandleRelaysIface ) {
s_running.remove( (HandleRelaysIface)running );
}
}
public static void SetRelayIDsHandler( HandleRelaysIface iface )
{
s_handler = iface;
}
private static boolean tryHandle( Uri data )
{
boolean handled = false;
if ( null != s_handler ) {
// This means the GamesList activity is frontmost
s_handler.handleInvite( data );
handled = true;
} else {
for ( HandleRelaysIface iface : s_running ) {
iface.handleInvite( data );
handled = true;
}
}
return handled;
}
public static boolean tryHandle( String[] relayIDs )
{
boolean handled = false;
if ( null != s_handler ) {
// This means the GamesList activity is frontmost
s_handler.handleRelaysIDs( relayIDs );
handled = true;
} else {
for ( HandleRelaysIface iface : s_running ) {
iface.handleRelaysIDs( relayIDs );
handled = true;
}
}
return handled;
}
public static boolean tryHandle( int gameID )
{
boolean handled = false;
if ( null != s_handler ) {
// This means the GamesList activity is frontmost
s_handler.handleGameID( gameID );
handled = true;
} else {
for ( HandleRelaysIface iface : s_running ) {
iface.handleGameID( gameID );
handled = true;
}
}
return handled;
}
} // onCreate
}

View file

@ -251,7 +251,7 @@ public class DlgDelegate {
public void doSyncMenuitem()
{
if ( null == DBUtils.getRelayIDs( m_activity, false ) ) {
if ( null == DBUtils.getRelayIDs( m_activity, null ) ) {
showOKOnlyDialog( R.string.no_games_to_refresh );
} else {
RelayReceiver.RestartTimer( m_activity, true );

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

@ -28,6 +28,11 @@ import com.google.android.gcm.GCMRegistrar;
public class GCMIntentService extends GCMBaseIntentService {
public GCMIntentService()
{
super( GCMConsts.SENDER_ID );
}
@Override
protected void onError( Context context, String error )
{
@ -37,12 +42,12 @@ public class GCMIntentService extends GCMBaseIntentService {
@Override
protected void onRegistered( Context context, String regId )
{
DbgUtils.logf("GCMIntentService.onRegistered(%s)", regId );
DbgUtils.logf( "GCMIntentService.onRegistered(%s)", regId );
XWPrefs.setGCMDevID( context, regId );
}
@Override
protected void onUnregistered( Context context, String regId )
protected void onUnregistered( Context context, String regId )
{
DbgUtils.logf( "GCMIntentService.onUnregistered(%s)", regId );
XWPrefs.clearGCMDevID( context );
@ -51,44 +56,43 @@ public class GCMIntentService extends GCMBaseIntentService {
@Override
protected void onMessage( Context context, Intent intent )
{
DbgUtils.logf( "GCMIntentService.onMessage(%s)", intent.toString() );
boolean doRestartTimer = true; // keep a few days...
String value = intent.getStringExtra( "msg" );
if ( null != value ) {
doRestartTimer = false; // expected key means new format
String title = intent.getStringExtra( "title" );
Utils.postNotification( context, null, title, value, 100000 );
}
String value;
value = intent.getStringExtra( "getMoves" );
if ( null != value && Boolean.parseBoolean( value ) ) {
doRestartTimer = true;
RelayReceiver.RestartTimer( context, true );
}
value = intent.getStringExtra( "checkUpdates" );
if ( null != value && Boolean.parseBoolean( value ) ) {
UpdateCheckReceiver.checkVersions( context, true );
}
if ( doRestartTimer ) {
RelayReceiver.RestartTimer( context, true );
value = intent.getStringExtra( "msg" );
if ( null != value ) {
String title = intent.getStringExtra( "title" );
if ( null != title ) {
int code = value.hashCode() ^ title.hashCode();
Utils.postNotification( context, null, title, value, code );
}
}
}
public static void init( Application app )
{
int sdkVersion = Integer.valueOf( android.os.Build.VERSION.SDK );
if ( 8 <= sdkVersion ) {
if ( 8 <= sdkVersion && 0 < GCMConsts.SENDER_ID.length() ) {
try {
GCMRegistrar.checkDevice( app );
// GCMRegistrar.checkManifest( app );
final String regId = GCMRegistrar.getRegistrationId( app );
String regId = XWPrefs.getGCMDevID( app );
if (regId.equals("")) {
GCMRegistrar.register( app, GCMConsts.SENDER_ID );
}
String curID = XWPrefs.getGCMDevID( app );
if ( ! curID.equals( regId ) ) {
XWPrefs.setGCMDevID( app, regId );
}
} catch ( UnsupportedOperationException uoe ) {
DbgUtils.logf( "Device can't do GCM." );
} catch ( Exception whatever ) {
// funky devices could do anything
DbgUtils.loge( whatever );
}
}
}

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
@ -20,316 +20,113 @@
package org.eehouse.android.xw4;
import android.content.Context;
import android.database.DataSetObserver;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.TextView;
import java.io.FileInputStream;
import java.text.DateFormat;
import java.util.Date;
import java.util.HashMap; // class is not synchronized
import java.util.Random;
import android.widget.ListView;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.*;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
public class GameListAdapter extends XWListAdapter {
private Context m_context;
private ListView m_list;
private LayoutInflater m_factory;
private int m_fieldID;
private Handler m_handler;
private static final boolean s_isFire;
private static Random s_random;
static {
s_isFire = Build.MANUFACTURER.equals( "Amazon" );
if ( s_isFire ) {
s_random = new Random();
}
}
private class ViewInfo implements View.OnClickListener {
private View m_view;
private View m_hideable;
private ExpiringTextView m_name;
private boolean m_expanded, m_haveTurn, m_haveTurnLocal;
private long m_rowid;
private long m_lastMoveTime;
private ImageButton m_expandButton;
public ViewInfo( View view, long rowid )
{
m_view = view;
m_rowid = rowid;
m_lastMoveTime = 0;
}
public ViewInfo( View view, long rowid, boolean expanded,
long lastMoveTime, boolean haveTurn,
boolean haveTurnLocal ) {
this( view, rowid );
m_expanded = expanded;
m_lastMoveTime = lastMoveTime;
m_haveTurn = haveTurn;
m_haveTurnLocal = haveTurnLocal;
m_hideable = (LinearLayout)view.findViewById( R.id.hideable );
m_name = (ExpiringTextView)m_view.findViewById( R.id.game_name );
m_expandButton = (ImageButton)view.findViewById( R.id.expander );
m_expandButton.setOnClickListener( this );
showHide();
}
private void showHide()
{
m_expandButton.setImageResource( m_expanded ?
R.drawable.expander_ic_maximized :
R.drawable.expander_ic_minimized);
m_hideable.setVisibility( m_expanded? View.VISIBLE : View.GONE );
m_name.setBackgroundColor( android.R.color.transparent );
m_name.setPct( m_handler, m_haveTurn && !m_expanded,
m_haveTurnLocal, m_lastMoveTime );
}
public void onClick( View view ) {
m_expanded = !m_expanded;
DBUtils.setExpanded( m_rowid, m_expanded );
showHide();
}
}
private HashMap<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 )
{
// DbgUtils.logf( "onPostExecute(rowid=%d)", m_rowid );
if ( -1 != m_rowid ) {
m_cb.itemLoaded( m_rowid );
}
}
} // class LoadItemTask
public GameListAdapter( Context context, Handler handler, LoadItemCB cb ) {
public GameListAdapter( Context context, ListView list,
Handler handler, LoadItemCB cb, String fieldName ) {
super( DBUtils.gamesList(context).length );
m_context = context;
m_list = list;
m_handler = handler;
m_cb = cb;
m_factory = LayoutInflater.from( context );
m_df = DateFormat.getDateTimeInstance( DateFormat.SHORT,
DateFormat.SHORT );
m_viewsCache = new HashMap<Long,ViewInfo>();
m_fieldID = fieldToID( fieldName );
}
@Override
public int getCount() {
return DBUtils.gamesList(m_context).length;
}
public Object getItem( int position )
// Views. A view depends on a summary, which takes time to load.
// When one needs loading it's done via an async task.
public View getView( int position, View convertView, ViewGroup parent )
{
final long rowid = DBUtils.gamesList(m_context)[position];
View layout;
boolean haveLayout = false;
synchronized( m_viewsCache ) {
ViewInfo vi = m_viewsCache.get( rowid );
haveLayout = null != vi;
if ( haveLayout ) {
layout = vi.m_view;
} else {
layout = m_factory.inflate( R.layout.game_list_tmp, null );
vi = new ViewInfo( layout, rowid );
m_viewsCache.put( rowid, vi );
}
}
if ( !haveLayout ) {
new LoadItemTask( m_context, rowid/*, ++m_taskCounter*/ ).execute();
}
// this doesn't work. Rather, it breaks highlighting because
// the background, if we don't set it, is a more complicated
// object like @android:drawable/list_selector_background. I
// tried calling getBackground(), expecting to get a Drawable
// I could then clone and modify, but null comes back. So
// layout must be inheriting its background from elsewhere or
// it gets set later, during layout.
// if ( (position%2) == 0 ) {
// layout.setBackgroundColor( 0xFF3F3F3F );
// }
return layout;
} // getItem
public View getView( int position, View convertView, ViewGroup parent ) {
return (View)getItem( position );
GameListItem result = (GameListItem)
m_factory.inflate( R.layout.game_list_item, null );
result.init( m_handler, DBUtils.gamesList(m_context)[position],
m_fieldID, m_cb );
return result;
}
public void inval( long rowid )
{
synchronized( m_viewsCache ) {
m_viewsCache.remove( rowid );
GameListItem child = getItemFor( rowid );
if ( null != child && child.getRowID() == rowid ) {
child.forceReload();
} else {
DbgUtils.logf( "no child for rowid %d", rowid );
GameListItem.inval( rowid );
m_list.invalidate();
}
}
public void setField( String field )
public void invalName( long rowid )
{
GameListItem item = getItemFor( rowid );
if ( null != item ) {
item.invalName();
}
}
public boolean setField( String fieldName )
{
boolean changed = false;
int newID = fieldToID( fieldName );
if ( -1 == newID ) {
if ( XWApp.DEBUG ) {
DbgUtils.logf( "GameListAdapter.setField(): unable to match"
+ " fieldName %s", fieldName );
}
} else if ( m_fieldID != newID ) {
if ( XWApp.DEBUG ) {
DbgUtils.logf( "setField: clearing views cache for change"
+ " from %d to %d", m_fieldID, newID );
}
m_fieldID = newID;
// return true so caller will do onContentChanged.
// There's no other way to signal GameListItem instances
// since we don't maintain a list of them.
changed = true;
}
return changed;
}
private GameListItem getItemFor( long rowid )
{
GameListItem result = null;
int position = positionFor( rowid );
if ( 0 <= position ) {
result = (GameListItem)m_list.getChildAt( position );
}
return result;
}
private int fieldToID( String fieldName )
{
int[] ids = {
R.string.game_summary_field_empty
@ -339,15 +136,24 @@ public class GameListAdapter extends XWListAdapter {
};
int result = -1;
for ( int id : ids ) {
if ( m_context.getString( id ).equals( field ) ) {
if ( m_context.getString( id ).equals( fieldName ) ) {
result = id;
break;
}
}
if ( m_fieldID != result ) {
m_viewsCache.clear();
m_fieldID = result;
}
return result;
}
private int positionFor( long rowid )
{
int position = -1;
long[] rowids = DBUtils.gamesList( m_context );
for ( int ii = 0; ii < rowids.length; ++ii ) {
if ( rowids[ii] == rowid ) {
position = ii;
break;
}
}
return position;
}
}

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

@ -24,19 +24,16 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.text.Html;
import android.text.TextUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import android.content.res.AssetManager;
import java.util.concurrent.locks.Lock;
import java.util.HashMap;
import java.util.HashSet;
import android.text.Html;
import java.util.concurrent.locks.Lock;
import org.json.JSONArray;
import org.json.JSONObject;
import junit.framework.Assert;
@ -49,134 +46,6 @@ public class GameUtils {
public static final String INTENT_KEY_ROWID = "rowid";
public static final String INTENT_FORRESULT_ROWID = "forresult";
// Implements read-locks and write-locks per game. A read lock is
// obtainable when other read locks are granted but not when a
// write lock is. Write-locks are exclusive.
public static class GameLock {
private long m_rowid;
private boolean m_isForWrite;
private int m_lockCount;
StackTraceElement[] m_lockTrace;
private static HashMap<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 )
@ -245,10 +114,16 @@ public class GameUtils {
public static void resetGame( Context context, long rowidIn )
{
GameLock lock = new GameLock( rowidIn, true ).lock();
tellDied( context, lock, true );
resetGame( context, lock, lock, false );
lock.unlock();
GameLock lock = new GameLock( rowidIn, true ).lock( 500 );
if ( null != lock ) {
tellDied( context, lock, true );
resetGame( context, lock, lock, false );
lock.unlock();
Utils.cancelNotification( context, (int)rowidIn );
} else {
DbgUtils.logf( "resetGame: unable to open rowid %d", rowidIn );
}
}
private static GameSummary summarizeAndClose( Context context,
@ -301,12 +176,17 @@ public class GameUtils {
public static long dupeGame( Context context, long rowidIn )
{
boolean juggle = CommonPrefs.getAutoJuggle( context );
GameLock lockSrc = new GameLock( rowidIn, false ).lock();
GameLock lockDest = resetGame( context, lockSrc, null, juggle );
long rowid = lockDest.getRowid();
lockDest.unlock();
lockSrc.unlock();
long rowid = DBUtils.ROWID_NOTFOUND;
GameLock lockSrc = new GameLock( rowidIn, false ).lock( 300 );
if ( null != lockSrc ) {
boolean juggle = CommonPrefs.getAutoJuggle( context );
GameLock lockDest = resetGame( context, lockSrc, null, juggle );
rowid = lockDest.getRowid();
lockDest.unlock();
lockSrc.unlock();
} else {
DbgUtils.logf( "dupeGame: unable to open rowid %d", rowidIn );
}
return rowid;
}
@ -318,6 +198,7 @@ public class GameUtils {
GameLock lock = new GameLock( rowid, true );
if ( lock.tryLock() ) {
tellDied( context, lock, informNow );
Utils.cancelNotification( context, (int)rowid );
DBUtils.deleteGame( context, lock );
lock.unlock();
} else {
@ -351,7 +232,8 @@ public class GameUtils {
String[] dictNames = gi.dictNames();
DictUtils.DictPairs pairs = DictUtils.openDicts( context, dictNames );
if ( pairs.anyMissing( dictNames ) ) {
DbgUtils.logf( "loadMakeGame() failing: dict unavailable" );
DbgUtils.logf( "loadMakeGame() failing: dicts %s unavailable",
TextUtils.join( ",", dictNames ) );
} else {
gamePtr = XwJNI.initJNI();
@ -415,7 +297,7 @@ public class GameUtils {
}
private static long makeNewMultiGame( Context context, CommsAddrRec addr,
int[] lang, String dict,
int[] lang, String[] dict,
int nPlayersT, int nPlayersH,
String inviteID, int gameID,
boolean isHost )
@ -423,8 +305,9 @@ public class GameUtils {
long rowid = -1;
CurGameInfo gi = new CurGameInfo( context, true );
gi.setLang( lang[0], dict );
gi.setLang( lang[0], dict[0] );
lang[0] = gi.dictLang;
dict[0] = gi.dictName;
gi.setNPlayers( nPlayersT, nPlayersH );
gi.juggle();
if ( 0 != gameID ) {
@ -449,7 +332,8 @@ public class GameUtils {
public static long makeNewNetGame( Context context, String room,
String inviteID, int[] lang,
int nPlayersT, int nPlayersH )
String[] dict, int nPlayersT,
int nPlayersH )
{
long rowid = -1;
String relayName = XWPrefs.getDefaultRelayHost( context );
@ -457,21 +341,24 @@ public class GameUtils {
CommsAddrRec addr = new CommsAddrRec( relayName, relayPort );
addr.ip_relay_invite = room;
return makeNewMultiGame( context, addr, lang, null, nPlayersT,
return makeNewMultiGame( context, addr, lang, dict, nPlayersT,
nPlayersH, inviteID, 0, false );
}
public static long makeNewNetGame( Context context, String room,
String inviteID, int lang, int nPlayers )
String inviteID, int lang, String dict,
int nPlayers )
{
int[] langarr = { lang };
return makeNewNetGame( context, room, inviteID, langarr, nPlayers, 1 );
String[] dictArr = { dict };
return makeNewNetGame( context, room, inviteID, langarr, dictArr,
nPlayers, 1 );
}
public static long makeNewNetGame( Context context, NetLaunchInfo info )
{
return makeNewNetGame( context, info.room, info.inviteID, info.lang,
info.nPlayers );
info.dict, info.nPlayersT );
}
public static long makeNewBTGame( Context context, int gameID,
@ -495,40 +382,26 @@ public class GameUtils {
{
long rowid = -1;
int[] langa = { lang };
String[] dicta = { dict };
boolean isHost = null == addr;
if ( isHost ) {
addr = new CommsAddrRec(CommsAddrRec.CommsConnType.COMMS_CONN_SMS);
}
return makeNewMultiGame( context, addr, langa, dict, nPlayersT,
return makeNewMultiGame( context, addr, langa, dicta, nPlayersT,
nPlayersH, null, gameID, isHost );
}
public static void launchBTInviter( Activity activity, int nMissing,
int requestCode )
{
Intent intent = new Intent( activity, BTInviteActivity.class );
intent.putExtra( BTInviteActivity.INTENT_KEY_NMISSING, nMissing );
activity.startActivityForResult( intent, requestCode );
}
public static void launchSMSInviter( Activity activity, int nMissing,
int requestCode )
{
Intent intent = new Intent( activity, SMSInviteActivity.class );
intent.putExtra( SMSInviteActivity.INTENT_KEY_NMISSING, nMissing );
activity.startActivityForResult( intent, requestCode );
}
public static void launchInviteActivity( Context context,
boolean choseEmail,
String room, String inviteID,
int lang, int nPlayers )
int lang, String dict,
int nPlayers )
{
if ( null == inviteID ) {
inviteID = makeRandomID();
}
Uri gameUri = NetLaunchInfo.makeLaunchUri( context, room, inviteID,
lang, nPlayers );
lang, dict, nPlayers );
if ( null != gameUri ) {
int fmtId = choseEmail? R.string.invite_htmf : R.string.invite_txtf;
@ -538,11 +411,28 @@ public class GameUtils {
Intent intent = new Intent();
if ( choseEmail ) {
intent.setAction( Intent.ACTION_SEND );
intent.setType( "message/rfc822");
String subject =
Utils.format( context, R.string.invite_subjectf, room );
intent.putExtra( Intent.EXTRA_SUBJECT, subject );
intent.putExtra( Intent.EXTRA_TEXT, Html.fromHtml(message) );
File attach = null;
File tmpdir = XWApp.ATTACH_SUPPORTED ?
DictUtils.getDownloadDir( context ) : null;
if ( null != tmpdir ) { // no attachment
attach = makeJsonFor( tmpdir, room, inviteID, lang,
dict, nPlayers );
}
if ( null == attach ) { // no attachment
intent.setType( "message/rfc822");
} else {
String mime = context.getString( R.string.invite_mime );
intent.setType( mime );
Uri uri = Uri.fromFile( attach );
intent.putExtra( Intent.EXTRA_STREAM, uri );
}
choiceID = R.string.invite_chooser_email;
} else {
intent.setAction( Intent.ACTION_VIEW );
@ -646,7 +536,6 @@ public class GameUtils {
boolean invited )
{
Intent intent = new Intent( activity, BoardActivity.class );
intent.setAction( Intent.ACTION_EDIT );
intent.putExtra( INTENT_KEY_ROWID, rowid );
if ( invited ) {
intent.putExtra( INVITED, true );
@ -696,39 +585,45 @@ public class GameUtils {
}
}
private static boolean feedMessages( Context context, long rowid,
byte[][] msgs, CommsAddrRec ret,
MultiMsgSink sink )
public static boolean feedMessages( Context context, long rowid,
byte[][] msgs, CommsAddrRec ret,
MultiMsgSink sink )
{
boolean draw = false;
Assert.assertTrue( -1 != rowid );
GameLock lock = new GameLock( rowid, true );
if ( lock.tryLock() ) {
CurGameInfo gi = new CurGameInfo( context );
FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid );
int gamePtr = loadMakeGame( context, gi, feedImpl, sink, lock );
XwJNI.comms_resendAll( gamePtr, false );
if ( null != msgs ) {
// timed lock: If a game is opened by BoardActivity just
// as we're trying to deliver this message to it it'll
// have the lock and we'll never get it. Better to drop
// the message than fire the hung-lock assert. Messages
// belong in local pre-delivery storage anyway.
GameLock lock = new GameLock( rowid, true ).lock( 150 );
if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( context );
FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid );
int gamePtr = loadMakeGame( context, gi, feedImpl, sink, lock );
if ( 0 != gamePtr ) {
XwJNI.comms_resendAll( gamePtr, false, false );
if ( null != msgs ) {
for ( byte[] msg : msgs ) {
draw = XwJNI.game_receiveMessage( gamePtr, msg, ret )
|| draw;
for ( byte[] msg : msgs ) {
draw = XwJNI.game_receiveMessage( gamePtr, msg, ret )
|| draw;
}
XwJNI.comms_ackAny( gamePtr );
// update gi to reflect changes due to messages
XwJNI.game_getGi( gamePtr, gi );
saveGame( context, gamePtr, gi, lock, false );
summarizeAndClose( context, lock, gamePtr, gi, feedImpl );
int flags = setFromFeedImpl( feedImpl );
if ( GameSummary.MSG_FLAGS_NONE != flags ) {
draw = true;
DBUtils.setMsgFlags( rowid, flags );
}
}
lock.unlock();
}
XwJNI.comms_ackAny( gamePtr );
// update gi to reflect changes due to messages
XwJNI.game_getGi( gamePtr, gi );
saveGame( context, gamePtr, gi, lock, false );
summarizeAndClose( context, lock, gamePtr, gi, feedImpl );
int flags = setFromFeedImpl( feedImpl );
if ( GameSummary.MSG_FLAGS_NONE != flags ) {
draw = true;
DBUtils.setMsgFlags( rowid, flags );
}
lock.unlock();
}
return draw;
} // feedMessages
@ -742,52 +637,45 @@ public class GameUtils {
return feedMessages( context, rowid, msgs, ret, sink );
}
// Current assumption: this is the relay case where return address
// can be null.
public static boolean feedMessages( Context context, String relayID,
byte[][] msgs, MultiMsgSink sink )
{
boolean draw = false;
long[] rowids = DBUtils.getRowIDsFor( context, relayID );
if ( null != rowids ) {
for ( long rowid : rowids ) {
draw = feedMessages( context, rowid, msgs, null, sink ) || draw;
}
}
return draw;
}
// This *must* involve a reset if the language is changing!!!
// Which isn't possible right now, so make sure the old and new
// dict have the same langauge code.
public static void replaceDicts( Context context, long rowid,
String oldDict, String newDict )
public static boolean replaceDicts( Context context, long rowid,
String oldDict, String newDict )
{
GameLock lock = new GameLock( rowid, true ).lock();
byte[] stream = savedGame( context, lock );
CurGameInfo gi = new CurGameInfo( context );
XwJNI.gi_from_stream( gi, stream );
GameLock lock = new GameLock( rowid, true ).lock(300);
boolean success = null != lock;
if ( success ) {
byte[] stream = savedGame( context, lock );
CurGameInfo gi = new CurGameInfo( context );
XwJNI.gi_from_stream( gi, stream );
// first time required so dictNames() will work
gi.replaceDicts( newDict );
// first time required so dictNames() will work
gi.replaceDicts( newDict );
String[] dictNames = gi.dictNames();
DictUtils.DictPairs pairs = DictUtils.openDicts( context, dictNames );
String[] dictNames = gi.dictNames();
DictUtils.DictPairs pairs = DictUtils.openDicts( context,
dictNames );
int gamePtr = XwJNI.initJNI();
XwJNI.game_makeFromStream( gamePtr, stream, gi, dictNames,
pairs.m_bytes, pairs.m_paths,
gi.langName(), JNIUtilsImpl.get(context),
CommonPrefs.get( context ) );
// second time required as game_makeFromStream can overwrite
gi.replaceDicts( newDict );
int gamePtr = XwJNI.initJNI();
XwJNI.game_makeFromStream( gamePtr, stream, gi, dictNames,
pairs.m_bytes, pairs.m_paths,
gi.langName(),
JNIUtilsImpl.get(context),
CommonPrefs.get( context ) );
// second time required as game_makeFromStream can overwrite
gi.replaceDicts( newDict );
saveGame( context, gamePtr, gi, lock, false );
saveGame( context, gamePtr, gi, lock, false );
summarizeAndClose( context, lock, gamePtr, gi );
summarizeAndClose( context, lock, gamePtr, gi );
lock.unlock();
}
lock.unlock();
} else {
DbgUtils.logf( "replaceDicts: unable to open rowid %d", rowid );
}
return success;
} // replaceDicts
public static void applyChanges( Context context, CurGameInfo gi,
CommsAddrRec car, GameLock lock,
@ -899,5 +787,31 @@ public class GameUtils {
}
}
private static File makeJsonFor( File dir, String room, String inviteID,
int lang, String dict, int nPlayers )
{
File result = null;
if ( XWApp.ATTACH_SUPPORTED ) {
JSONObject json = new JSONObject();
try {
json.put( MultiService.ROOM, room );
json.put( MultiService.INVITEID, inviteID );
json.put( MultiService.LANG, lang );
json.put( MultiService.DICT, dict );
json.put( MultiService.NPLAYERST, nPlayers );
byte[] data = json.toString().getBytes();
File file = new File( dir,
String.format("invite_%s", room ) );
FileOutputStream fos = new FileOutputStream( file );
fos.write( data, 0, data.length );
fos.close();
result = file;
} catch ( Exception ex ) {
DbgUtils.loge( ex );
}
}
return result;
}
}

View file

@ -20,30 +20,31 @@
package org.eehouse.android.xw4;
import android.app.ListActivity;
import android.app.Dialog;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ListActivity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.Button;
import android.view.MenuInflater;
import java.io.File;
import android.preference.PreferenceManager;
import java.util.Date;
// import android.telephony.PhoneStateListener;
// import android.telephony.TelephonyManager;
import junit.framework.Assert;
@ -51,20 +52,24 @@ import junit.framework.Assert;
import org.eehouse.android.xw4.jni.*;
public class GamesList extends XWListActivity
implements DispatchNotify.HandleRelaysIface,
DBUtils.DBChangeListener,
implements DBUtils.DBChangeListener,
GameListAdapter.LoadItemCB,
NetUtils.DownloadFinishedListener {
DictImportActivity.DownloadFinishedListener {
private static final int WARN_NODICT = DlgDelegate.DIALOG_LAST + 1;
private static final int WARN_NODICT_SUBST = WARN_NODICT + 1;
private static final int SHOW_SUBST = WARN_NODICT + 2;
private static final int GET_NAME = WARN_NODICT + 3;
private static final int RENAME_GAME = WARN_NODICT + 4;
private static final int WARN_NODICT_NEW = WARN_NODICT + 2;
private static final int SHOW_SUBST = WARN_NODICT + 3;
private static final int GET_NAME = WARN_NODICT + 4;
private static final int RENAME_GAME = WARN_NODICT + 5;
private static final String SAVE_ROWID = "SAVE_ROWID";
private static final String SAVE_DICTNAMES = "SAVE_DICTNAMES";
private static final String RELAYIDS_EXTRA = "relayids";
private static final String GAMEID_EXTRA = "gameid";
private static final String REMATCH_ROWID_EXTRA = "rowid";
private static final int NEW_NET_GAME_ACTION = 1;
private static final int RESET_GAME_ACTION = 2;
private static final int DELETE_GAME_ACTION = 3;
@ -80,14 +85,12 @@ public class GamesList extends XWListActivity
private GameListAdapter m_adapter;
private String m_missingDict;
private String[] m_missingDictNames;
private long m_missingDictRowId;
private String m_missingDictName;
private long m_missingDictRowId = DBUtils.ROWID_NOTFOUND;
private String[] m_sameLangDicts;
private int m_missingDictLang;
private long m_rowid;
private String m_nameField;
private NetLaunchInfo m_netLaunchInfo;
// private String m_smsPhone;
@Override
protected Dialog onCreateDialog( int id )
@ -100,14 +103,21 @@ public class GamesList extends XWListActivity
AlertDialog.Builder ab;
switch ( id ) {
case WARN_NODICT:
case WARN_NODICT_NEW:
case WARN_NODICT_SUBST:
lstnr = new DialogInterface.OnClickListener() {
public void onClick( DialogInterface dlg, int item ) {
// just do one
NetUtils.downloadDictInBack( GamesList.this,
// no name, so user must pick
if ( null == m_missingDictName ) {
DictsActivity.launchAndDownload( GamesList.this,
m_missingDictLang );
} else {
DictImportActivity
.downloadDictInBack( GamesList.this,
m_missingDictLang,
m_missingDictNames[0],
m_missingDictName,
GamesList.this );
}
}
};
String message;
@ -117,16 +127,20 @@ public class GamesList extends XWListActivity
if ( WARN_NODICT == id ) {
message = getString( R.string.no_dictf,
gameName, langName );
} else if ( WARN_NODICT_NEW == id ) {
message =
getString( R.string.invite_dict_missing_body_nonamef,
null, m_missingDictName, langName );
} else {
message = getString( R.string.no_dict_substf,
gameName, m_missingDictNames[0],
gameName, m_missingDictName,
langName );
}
ab = new AlertDialog.Builder( this )
.setTitle( R.string.no_dict_title )
.setMessage( message )
.setPositiveButton( R.string.button_ok, null )
.setPositiveButton( R.string.button_cancel, null )
.setNegativeButton( R.string.button_download, lstnr )
;
if ( WARN_NODICT_SUBST == id ) {
@ -150,10 +164,12 @@ public class GamesList extends XWListActivity
getCheckedItemPosition();
String dict = m_sameLangDicts[pos];
dict = DictLangCache.stripCount( dict );
GameUtils.replaceDicts( GamesList.this,
m_missingDictRowId,
m_missingDictNames[0],
dict );
if ( GameUtils.replaceDicts( GamesList.this,
m_missingDictRowId,
m_missingDictName,
dict ) ) {
launchGameIf();
}
}
};
dialog = new AlertDialog.Builder( this )
@ -184,8 +200,7 @@ public class GamesList extends XWListActivity
public void onClick( DialogInterface dlg, int item ) {
String name = namerView.getName();
DBUtils.setName( GamesList.this, m_rowid, name );
m_adapter.inval( m_rowid );
onContentChanged();
m_adapter.invalName( m_rowid );
}
};
dialog = new AlertDialog.Builder( this )
@ -266,7 +281,9 @@ public class GamesList extends XWListActivity
}
});
m_adapter = new GameListAdapter( this, new Handler(), this );
String field = CommonPrefs.getSummaryField( this );
m_adapter = new GameListAdapter( this, getListView(), new Handler(),
this, field );
setListAdapter( m_adapter );
NetUtils.informOfDeaths( this );
@ -275,6 +292,7 @@ public class GamesList extends XWListActivity
startFirstHasDict( intent );
startNewNetGame( intent );
startHasGameID( intent );
startHasRowID( intent );
askDefaultNameIf();
} // onCreate
@ -285,18 +303,17 @@ public class GamesList extends XWListActivity
{
super.onNewIntent( intent );
Assert.assertNotNull( intent );
invalRelayIDs( intent.
getStringArrayExtra( DispatchNotify.RELAYIDS_EXTRA ) );
invalRelayIDs( intent.getStringArrayExtra( RELAYIDS_EXTRA ) );
startFirstHasDict( intent );
startNewNetGame( intent );
startHasGameID( intent );
startHasRowID( intent );
}
@Override
protected void onStart()
{
super.onStart();
DispatchNotify.SetRelayIDsHandler( this );
boolean hide = CommonPrefs.getHideIntro( this );
int hereOrGone = hide ? View.GONE : View.VISIBLE;
@ -323,7 +340,6 @@ public class GamesList extends XWListActivity
// (TelephonyManager)getSystemService( Context.TELEPHONY_SERVICE );
// mgr.listen( m_phoneStateListener, PhoneStateListener.LISTEN_NONE );
// m_phoneStateListener = null;
DispatchNotify.SetRelayIDsHandler( null );
super.onStop();
}
@ -340,7 +356,7 @@ public class GamesList extends XWListActivity
{
super.onSaveInstanceState( outState );
outState.putLong( SAVE_ROWID, m_rowid );
outState.putStringArray( SAVE_DICTNAMES, m_missingDictNames );
outState.putString( SAVE_DICTNAMES, m_missingDictName );
if ( null != m_netLaunchInfo ) {
m_netLaunchInfo.putSelf( outState );
}
@ -351,7 +367,7 @@ public class GamesList extends XWListActivity
if ( null != bundle ) {
m_rowid = bundle.getLong( SAVE_ROWID );
m_netLaunchInfo = new NetLaunchInfo( bundle );
m_missingDictNames = bundle.getStringArray( SAVE_DICTNAMES );
m_missingDictName = bundle.getString( SAVE_DICTNAMES );
}
}
@ -364,62 +380,26 @@ public class GamesList extends XWListActivity
}
}
// DispatchNotify.HandleRelaysIface interface
public void handleRelaysIDs( final String[] relayIDs )
{
post( new Runnable() {
public void run() {
invalRelayIDs( relayIDs );
startFirstHasDict( relayIDs );
}
} );
}
public void handleInvite( Uri invite )
{
final NetLaunchInfo nli = new NetLaunchInfo( invite );
if ( nli.isValid() ) {
post( new Runnable() {
@Override
public void run() {
startNewNetGame( nli );
}
} );
}
}
public void handleGameID( final int gameID )
{
post( new Runnable() {
public void run() {
startHasGameID( gameID );
}
} );
}
// DBUtils.DBChangeListener interface
public void gameSaved( final long rowid )
public void gameSaved( final long rowid, final boolean countChanged )
{
post( new Runnable() {
public void run() {
m_adapter.inval( rowid );
onContentChanged();
if ( countChanged ) {
onContentChanged();
} else {
m_adapter.inval( rowid );
}
}
} );
}
// GameListAdapter.LoadItemCB interface
public void itemLoaded( long rowid )
{
onContentChanged();
}
public void itemClicked( long rowid )
public void itemClicked( long rowid, GameSummary summary )
{
// We need a way to let the user get back to the basic-config
// dialog in case it was dismissed. That way it to check for
// an empty room name.
GameSummary summary = DBUtils.getSummary( this, rowid );
if ( summary.conType == CommsAddrRec.CommsConnType.COMMS_CONN_RELAY
&& summary.roomName.length() == 0 ) {
// If it's unconfigured and of the type RelayGameActivity
@ -434,12 +414,12 @@ public class GamesList extends XWListActivity
GameUtils.doConfig( this, rowid, clazz );
} else {
if ( checkWarnNoDict( rowid ) ) {
GameUtils.launchGame( this, rowid );
launchGame( rowid );
}
}
}
// BTService.BTEventListener interface
// BTService.MultiEventListener interface
@Override
public void eventOccurred( MultiService.MultiEvent event,
final Object ... args )
@ -466,11 +446,13 @@ public class GamesList extends XWListActivity
if ( AlertDialog.BUTTON_POSITIVE == which ) {
switch( id ) {
case NEW_NET_GAME_ACTION:
long rowid = GameUtils.makeNewNetGame( this, m_netLaunchInfo );
GameUtils.launchGame( this, rowid, true );
if ( checkWarnNoDict( m_netLaunchInfo ) ) {
makeNewNetGameIf();
}
break;
case RESET_GAME_ACTION:
GameUtils.resetGame( this, m_rowid );
onContentChanged(); // required because position may change
break;
case DELETE_GAME_ACTION:
GameUtils.deleteGame( this, m_rowid, true );
@ -479,7 +461,6 @@ public class GamesList extends XWListActivity
long[] games = DBUtils.gamesList( this );
for ( int ii = games.length - 1; ii >= 0; --ii ) {
GameUtils.deleteGame( this, games[ii], ii == 0 );
m_adapter.inval( games[ii] );
}
break;
case SYNC_MENU_ACTION:
@ -616,14 +597,20 @@ public class GamesList extends XWListActivity
return handled;
}
// NetUtils.DownloadFinishedListener interface
// DictImportActivity.DownloadFinishedListener interface
public void downloadFinished( String name, final boolean success )
{
post( new Runnable() {
public void run() {
int id = success ? R.string.download_done
: R.string.download_failed;
Utils.showToast( GamesList.this, id );
boolean madeGame = false;
if ( success ) {
madeGame = makeNewNetGameIf() || launchGameIf();
}
if ( ! madeGame ) {
int id = success ? R.string.download_done
: R.string.download_failed;
Utils.showToast( GamesList.this, id );
}
}
} );
}
@ -664,8 +651,7 @@ public class GamesList extends XWListActivity
showOKOnlyDialog( R.string.no_copy_network );
} else {
byte[] stream = GameUtils.savedGame( this, m_rowid );
GameUtils.GameLock lock =
GameUtils.saveNewGame( this, stream );
GameLock lock = GameUtils.saveNewGame( this, stream );
DBUtils.saveSummary( this, lock, summary );
lock.unlock();
}
@ -690,21 +676,57 @@ public class GamesList extends XWListActivity
return handled;
} // handleMenuItem
private boolean checkWarnNoDict( NetLaunchInfo nli )
{
// check that we have the dict required
boolean haveDict;
if ( null == nli.dict ) { // can only test for language support
String[] dicts = DictLangCache.getHaveLang( this, nli.lang );
haveDict = 0 < dicts.length;
if ( haveDict ) {
// Just pick one -- good enough for the period when
// users aren't using new clients that include the
// dict name.
nli.dict = dicts[0];
}
} else {
haveDict =
DictLangCache.haveDict( this, nli.lang, nli.dict );
}
if ( !haveDict ) {
m_netLaunchInfo = nli;
m_missingDictLang = nli.lang;
m_missingDictName = nli.dict;
showDialog( WARN_NODICT_NEW );
}
return haveDict;
}
private boolean checkWarnNoDict( long rowid )
{
String[][] missingNames = new String[1][];
int[] missingLang = new int[1];
boolean hasDicts = GameUtils.gameDictsHere( this, rowid,
missingNames,
missingLang );
boolean hasDicts =
GameUtils.gameDictsHere( this, rowid, missingNames, missingLang );
if ( !hasDicts ) {
m_missingDictNames = missingNames[0];
m_missingDictLang = missingLang[0];
if ( 0 < missingNames[0].length ) {
m_missingDictName = missingNames[0][0];
} else {
m_missingDictName = null;
}
m_missingDictRowId = rowid;
if ( 0 == DictLangCache.getLangCount( this, m_missingDictLang ) ) {
showDialog( WARN_NODICT );
} else {
} else if ( null != m_missingDictName ) {
showDialog( WARN_NODICT_SUBST );
} else {
String dict =
DictLangCache.getHaveLang( this, m_missingDictLang)[0];
if ( GameUtils.replaceDicts( this, m_missingDictRowId,
null, dict ) ) {
launchGameIf();
}
}
}
return hasDicts;
@ -721,7 +743,6 @@ public class GamesList extends XWListActivity
}
}
}
onContentChanged();
}
}
@ -736,7 +757,7 @@ public class GamesList extends XWListActivity
if ( null != rowids ) {
for ( long rowid : rowids ) {
if ( GameUtils.gameDictsHere( this, rowid ) ) {
GameUtils.launchGame( this, rowid );
launchGame( rowid );
break outer;
}
}
@ -748,8 +769,7 @@ public class GamesList extends XWListActivity
private void startFirstHasDict( Intent intent )
{
if ( null != intent ) {
String[] relayIDs =
intent.getStringArrayExtra( DispatchNotify.RELAYIDS_EXTRA );
String[] relayIDs = intent.getStringArrayExtra( RELAYIDS_EXTRA );
startFirstHasDict( relayIDs );
}
}
@ -759,47 +779,64 @@ public class GamesList extends XWListActivity
startActivity( new Intent( this, NewGameActivity.class ) );
}
private void startNewNetGame( NetLaunchInfo info )
private void startNewNetGame( NetLaunchInfo nli )
{
long rowid = DBUtils.getRowIDForOpen( this, info );
Date create = DBUtils.getMostRecentCreate( this, nli );
if ( DBUtils.ROWID_NOTFOUND == rowid ) {
rowid = GameUtils.makeNewNetGame( this, info );
GameUtils.launchGame( this, rowid, true );
if ( null == create ) {
if ( checkWarnNoDict( nli ) ) {
makeNewNetGame( nli );
}
} else {
String msg = getString( R.string.dup_game_queryf, info.room );
m_netLaunchInfo = info;
String msg = getString( R.string.dup_game_queryf,
create.toString() );
m_netLaunchInfo = nli;
showConfirmThen( msg, NEW_NET_GAME_ACTION );
}
} // startNewNetGame
private void startNewNetGame( Intent intent )
{
Uri data = intent.getData();
if ( null != data ) {
NetLaunchInfo info = new NetLaunchInfo( data );
if ( info.isValid() ) {
startNewNetGame( info );
NetLaunchInfo nli = null;
if ( MultiService.isMissingDictIntent( intent ) ) {
nli = new NetLaunchInfo( intent );
} else {
Uri data = intent.getData();
if ( null != data ) {
nli = new NetLaunchInfo( this, data );
}
}
if ( null != nli && nli.isValid() ) {
startNewNetGame( nli );
}
} // startNewNetGame
private void startHasGameID( int gameID )
{
long[] rowids = DBUtils.getRowIDsFor( this, gameID );
if ( null != rowids && 0 < rowids.length ) {
GameUtils.launchGame( this, rowids[0] );
launchGame( rowids[0] );
}
}
private void startHasGameID( Intent intent )
{
int gameID = intent.getIntExtra( DispatchNotify.GAMEID_EXTRA, 0 );
int gameID = intent.getIntExtra( GAMEID_EXTRA, 0 );
if ( 0 != gameID ) {
startHasGameID( gameID );
}
}
private void startHasRowID( Intent intent )
{
long rowid = intent.getLongExtra( REMATCH_ROWID_EXTRA, -1 );
if ( -1 != rowid ) {
// this will juggle if the preference is set
long newid = GameUtils.dupeGame( this, rowid );
launchGame( newid );
}
}
private void askDefaultNameIf()
{
if ( null == CommonPrefs.getDefaultPlayerName( this, 0, false ) ) {
@ -812,10 +849,96 @@ public class GamesList extends XWListActivity
private void updateField()
{
String newField = CommonPrefs.getSummaryField( this );
if ( ! newField.equals( m_nameField ) ) {
m_nameField = newField;
m_adapter.setField( newField );
if ( m_adapter.setField( newField ) ) {
// The adapter should be able to decide whether full
// content change is required. PENDING
onContentChanged();
}
}
private boolean makeNewNetGameIf()
{
boolean madeGame = null != m_netLaunchInfo;
if ( madeGame ) {
makeNewNetGame( m_netLaunchInfo );
m_netLaunchInfo = null;
}
return madeGame;
}
private boolean launchGameIf()
{
boolean madeGame = DBUtils.ROWID_NOTFOUND != m_missingDictRowId;
if ( madeGame ) {
GameUtils.launchGame( this, m_missingDictRowId );
m_missingDictRowId = DBUtils.ROWID_NOTFOUND;
}
return madeGame;
}
private void launchGame( long rowid, boolean invited )
{
GameUtils.launchGame( this, rowid, invited );
}
private void launchGame( long rowid )
{
launchGame( rowid, false );
}
private void makeNewNetGame( NetLaunchInfo info )
{
long rowid = GameUtils.makeNewNetGame( this, info );
launchGame( rowid, true );
}
public static void onGameDictDownload( Context context, Intent intent )
{
intent.setClass( context, GamesList.class );
context.startActivity( intent );
}
private static Intent makeSelfIntent( Context context )
{
Intent intent = new Intent( context, GamesList.class );
intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_NEW_TASK );
return intent;
}
public static Intent makeRelayIdsIntent( Context context,
String[] relayIDs )
{
Intent intent = makeSelfIntent( context );
intent.putExtra( RELAYIDS_EXTRA, relayIDs );
return intent;
}
public static Intent makeGameIDIntent( Context context, int gameID )
{
Intent intent = makeSelfIntent( context );
intent.putExtra( GAMEID_EXTRA, gameID );
return intent;
}
public static Intent makeRematchIntent( Context context, CurGameInfo gi,
long rowid )
{
Intent intent = makeSelfIntent( context );
if ( CurGameInfo.DeviceRole.SERVER_STANDALONE == gi.serverRole ) {
intent.putExtra( REMATCH_ROWID_EXTRA, rowid );
} else {
Utils.notImpl( context );
}
return intent;
}
public static void openGame( Context context, Uri data )
{
Intent intent = makeSelfIntent( context );
intent.setData( data );
context.startActivity( intent );
}
}

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

@ -31,6 +31,8 @@ public class MultiService {
public static final String LANG = "LANG";
public static final String DICT = "DICT";
public static final String GAMEID = "GAMEID";
public static final String INVITEID = "INVITEID"; // relay only
public static final String ROOM = "ROOM";
public static final String GAMENAME = "GAMENAME";
public static final String NPLAYERST = "NPLAYERST";
public static final String NPLAYERSH = "NPLAYERSH";
@ -38,8 +40,9 @@ public class MultiService {
public static final String OWNER = "OWNER";
public static final int OWNER_SMS = 1;
public static final int OWNER_RELAY = 2;
private BTEventListener m_li;
private MultiEventListener m_li;
public enum MultiEvent { BAD_PROTO
, BT_ENABLED
@ -61,14 +64,14 @@ public class MultiService {
, SMS_SEND_FAILED_NORADIO
};
public interface BTEventListener {
public interface MultiEventListener {
public void eventOccurred( MultiEvent event, Object ... args );
}
// public interface MultiEventSrc {
// public void setBTEventListener( BTEventListener li );
// }
public void setListener( BTEventListener li )
public void setListener( MultiEventListener li )
{
synchronized( this ) {
m_li = li;
@ -84,11 +87,39 @@ public class MultiService {
}
}
public static void fillInviteIntent( Intent intent, String gameName,
int lang, String dict,
int nPlayersT, int nPlayersH )
{
intent.putExtra( GAMENAME, gameName );
intent.putExtra( LANG, lang );
intent.putExtra( DICT, dict );
intent.putExtra( NPLAYERST, nPlayersT ); // both of these used
intent.putExtra( NPLAYERSH, nPlayersH );
}
public static Intent makeMissingDictIntent( Context context, String gameName,
int lang, String dict,
int nPlayersT, int nPlayersH )
{
Intent intent = new Intent( context, DictsActivity.class );
fillInviteIntent( intent, gameName, lang, dict, nPlayersT, nPlayersH );
return intent;
}
public static Intent makeMissingDictIntent( Context context, NetLaunchInfo nli )
{
Intent intent = makeMissingDictIntent( context, null, nli.lang, nli.dict,
nli.nPlayersT, 1 );
intent.putExtra( ROOM, nli.room );
return intent;
}
public static boolean isMissingDictIntent( Intent intent )
{
return intent.hasExtra( LANG )
&& intent.hasExtra( DICT )
&& intent.hasExtra( GAMEID )
// && intent.hasExtra( DICT )
&& (intent.hasExtra( GAMEID ) || intent.hasExtra( ROOM ))
&& intent.hasExtra( GAMENAME )
&& intent.hasExtra( NPLAYERST )
&& intent.hasExtra( NPLAYERSH );
@ -102,8 +133,10 @@ public class MultiService {
String langStr = DictLangCache.getLangName( context, lang );
String dict = intent.getStringExtra( DICT );
String inviter = intent.getStringExtra( INVITER );
String msg = context.getString( R.string.invite_dict_missing_bodyf,
inviter, dict, langStr );
int msgID = (null == inviter) ? R.string.invite_dict_missing_body_nonamef
: R.string.invite_dict_missing_bodyf;
String msg = context.getString( msgID, inviter, dict, langStr );
return new AlertDialog.Builder( context )
.setTitle( R.string.invite_dict_missing_title )
.setMessage( msg)
@ -112,6 +145,13 @@ public class MultiService {
.create();
}
public static void postMissingDictNotification( Context content,
Intent intent, int id )
{
Utils.postNotification( content, intent, R.string.missing_dict_title,
R.string.missing_dict_detail, id );
}
// resend the intent, but only if the dict it names is here. (If
// it's not, we may need to try again later, e.g. because our cue
// was a focus gain.)
@ -123,11 +163,15 @@ public class MultiService {
String dict = intent.getStringExtra( DICT );
downloaded = DictLangCache.haveDict( context, lang, dict );
if ( downloaded ) {
int owner = intent.getIntExtra( OWNER, -1 );
if ( owner == OWNER_SMS ) {
switch ( intent.getIntExtra( OWNER, -1 ) ) {
case OWNER_SMS:
SMSService.onGameDictDownload( context, intent );
} else {
DbgUtils.logf( "unexpected OWNER: %d", owner );
break;
case OWNER_RELAY:
GamesList.onGameDictDownload( context, intent );
break;
default:
DbgUtils.logf( "unexpected OWNER" );
}
}
}

View file

@ -20,21 +20,27 @@
package org.eehouse.android.xw4;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.net.Uri.Builder;
import android.os.Bundle;
import java.net.URLEncoder;
import java.io.InputStream;
import org.json.JSONObject;
import junit.framework.Assert;
public class NetLaunchInfo {
public String room;
public String inviteID;
public String dict;
public int lang;
public int nPlayers;
public int nPlayersT;
private static final String LANG = "netlaunchinfo_lang";
private static final String ROOM = "netlaunchinfo_room";
private static final String DICT = "netlaunchinfo_dict";
private static final String INVITEID = "netlaunchinfo_inviteid";
private static final String NPLAYERS = "netlaunchinfo_nplayers";
private static final String VALID = "netlaunchinfo_valid";
@ -46,30 +52,50 @@ public class NetLaunchInfo {
bundle.putInt( LANG, lang );
bundle.putString( ROOM, room );
bundle.putString( INVITEID, inviteID );
bundle.putInt( NPLAYERS, nPlayers );
bundle.putString( DICT, dict );
bundle.putInt( NPLAYERS, nPlayersT );
bundle.putBoolean( VALID, m_valid );
}
public NetLaunchInfo( Bundle bundle )
{
lang = bundle.getInt( LANG );
lang = bundle.getInt( LANG );
room = bundle.getString( ROOM );
dict = bundle.getString( DICT );
inviteID = bundle.getString( INVITEID );
nPlayers = bundle.getInt( NPLAYERS );
m_valid = bundle.getBoolean( VALID );
nPlayersT = bundle.getInt( NPLAYERS );
m_valid = bundle.getBoolean( VALID );
}
public NetLaunchInfo( Uri data )
public NetLaunchInfo( Context context, Uri data )
{
m_valid = false;
if ( null != data ) {
String scheme = data.getScheme();
try {
room = data.getQueryParameter( "room" );
inviteID = data.getQueryParameter( "id" );
String langStr = data.getQueryParameter( "lang" );
lang = Integer.decode( langStr );
String np = data.getQueryParameter( "np" );
nPlayers = Integer.decode( np );
if ( "content".equals(scheme) || "file".equals(scheme) ) {
Assert.assertNotNull( context );
ContentResolver resolver = context.getContentResolver();
InputStream is = resolver.openInputStream( data );
int len = is.available();
byte[] buf = new byte[len];
is.read( buf );
JSONObject json = new JSONObject( new String( buf ) );
room = json.getString( MultiService.ROOM );
inviteID = json.getString( MultiService.INVITEID );
lang = json.getInt( MultiService.LANG );
dict = json.getString( MultiService.DICT );
nPlayersT = json.getInt( MultiService.NPLAYERST );
} else {
room = data.getQueryParameter( "room" );
inviteID = data.getQueryParameter( "id" );
dict = data.getQueryParameter( "wl" );
String langStr = data.getQueryParameter( "lang" );
lang = Integer.decode( langStr );
String np = data.getQueryParameter( "np" );
nPlayersT = Integer.decode( np );
}
m_valid = true;
} catch ( Exception e ) {
DbgUtils.logf( "unable to parse \"%s\"", data.toString() );
@ -77,18 +103,34 @@ public class NetLaunchInfo {
}
}
public static Uri makeLaunchUri( Context context, String room,
String inviteID, int lang, int nPlayers )
public NetLaunchInfo( Intent intent )
{
Builder ub = new Builder();
ub.scheme( "http" );
ub.path( context.getString( R.string.game_url_pathf,
XWPrefs.getDefaultRedirHost( context ) ) );
ub.appendQueryParameter( "lang", String.format("%d", lang ) );
ub.appendQueryParameter( "np", String.format( "%d", nPlayers ) );
ub.appendQueryParameter( "room", room );
ub.appendQueryParameter( "id", inviteID );
room = intent.getStringExtra( MultiService.ROOM );
inviteID = intent.getStringExtra( MultiService.INVITEID );
lang = intent.getIntExtra( MultiService.LANG, -1 );
dict = intent.getStringExtra( MultiService.DICT );
nPlayersT = intent.getIntExtra( MultiService.NPLAYERST, -1 );
m_valid = null != room
&& -1 != lang
&& -1 != nPlayersT;
}
public static Uri makeLaunchUri( Context context, String room,
String inviteID, int lang,
String dict, int nPlayersT )
{
Uri.Builder ub = new Uri.Builder()
.scheme( "http" )
.path( String.format( "//%s%s",
context.getString(R.string.invite_host),
context.getString(R.string.invite_prefix) ) )
.appendQueryParameter( "lang", String.format("%d", lang ) )
.appendQueryParameter( "np", String.format( "%d", nPlayersT ) )
.appendQueryParameter( "room", room )
.appendQueryParameter( "id", inviteID );
if ( null != dict ) {
ub.appendQueryParameter( "wl", dict );
}
return ub.build();
}

View file

@ -21,27 +21,14 @@
package org.eehouse.android.xw4;
import android.content.Context;
import android.os.Handler;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.Socket;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import javax.net.SocketFactory;
public class NetUtils {
private static final int MAX_SEND = 1024;
private static final int MAX_BUF = MAX_SEND - 2;
public static final byte PROTOCOL_VERSION = 0;
// from xwrelay.h
public static byte PRX_PUB_ROOMS = 1;
@ -50,10 +37,6 @@ public class NetUtils {
public static byte PRX_GET_MSGS = 4;
public static byte PRX_PUT_MSGS = 5;
public interface DownloadFinishedListener {
void downloadFinished( String name, boolean success );
}
public static Socket makeProxySocket( Context context,
int timeoutMillis )
{
@ -140,8 +123,7 @@ public class NetUtils {
}
}
public static byte[][][] queryRelay( Context context, String[] ids,
int nBytes )
public static byte[][][] queryRelay( Context context, String[] ids )
{
byte[][][] msgs = null;
try {
@ -151,6 +133,7 @@ public class NetUtils {
new DataOutputStream( socket.getOutputStream() );
// total packet size
int nBytes = sumStrings( ids );
outStream.writeShort( 2 + nBytes + ids.length + 1 );
outStream.writeByte( NetUtils.PROTOCOL_VERSION );
@ -204,132 +187,15 @@ public class NetUtils {
return msgs;
} // queryRelay
public static void sendToRelay( Context context,
HashMap<String,ArrayList<byte[]>> msgHash )
private static int sumStrings( final String[] strs )
{
// format: total msg lenth: 2
// number-of-relayIDs: 2
// for-each-relayid: relayid + '\n': varies
// message count: 1
// for-each-message: length: 2
// message: varies
if ( null != msgHash ) {
try {
// Build up a buffer containing everything but the total
// message length and number of relayIDs in the message.
ByteArrayOutputStream store =
new ByteArrayOutputStream( MAX_BUF ); // mem
DataOutputStream outBuf = new DataOutputStream( store );
int msgLen = 4; // relayID count + protocol stuff
int nRelayIDs = 0;
Iterator<String> iter = msgHash.keySet().iterator();
while ( iter.hasNext() ) {
String relayID = iter.next();
int thisLen = 1 + relayID.length(); // string and '\n'
thisLen += 2; // message count
ArrayList<byte[]> msgs = msgHash.get( relayID );
for ( byte[] msg : msgs ) {
thisLen += 2 + msg.length;
}
if ( msgLen + thisLen > MAX_BUF ) {
// Need to deal with this case by sending multiple
// packets. It WILL happen.
break;
}
// got space; now write it
++nRelayIDs;
outBuf.writeBytes( relayID );
outBuf.write( '\n' );
outBuf.writeShort( msgs.size() );
for ( byte[] msg : msgs ) {
outBuf.writeShort( msg.length );
outBuf.write( msg );
}
msgLen += thisLen;
}
// Now open a real socket, write size and proto, and
// copy in the formatted buffer
Socket socket = makeProxySocket( context, 8000 );
if ( null != socket ) {
DataOutputStream outStream =
new DataOutputStream( socket.getOutputStream() );
outStream.writeShort( msgLen );
outStream.writeByte( NetUtils.PROTOCOL_VERSION );
outStream.writeByte( NetUtils.PRX_PUT_MSGS );
outStream.writeShort( nRelayIDs );
outStream.write( store.toByteArray() );
outStream.flush();
socket.close();
}
} catch ( java.io.IOException ioe ) {
DbgUtils.loge( ioe );
int len = 0;
if ( null != strs ) {
for ( String str : strs ) {
len += str.length();
}
} else {
DbgUtils.logf( "sendToRelay: null msgs" );
}
} // sendToRelay
static void downloadDictInBack( Context context, int lang, String name,
DownloadFinishedListener lstnr )
{
DictUtils.DictLoc loc = XWPrefs.getDefaultLoc( context );
downloadDictInBack( context, lang, name, loc, lstnr );
}
static void downloadDictInBack( Context context, int lang, String name,
DictUtils.DictLoc loc,
DownloadFinishedListener lstnr )
{
String url = Utils.makeDictUrl( context, lang, name );
downloadDictInBack( context, url, loc, lstnr );
}
static void downloadDictInBack( final Context context, final String urlStr,
final DictUtils.DictLoc loc,
final DownloadFinishedListener lstnr )
{
String tmp = Utils.dictFromURL( context, urlStr );
final String name = DictUtils.removeDictExtn( tmp );
String msg = context.getString( R.string.downloadingf, name );
final StatusNotifier sno =
new StatusNotifier( context, msg, R.string.download_done );
new Thread( new Runnable() {
public void run() {
boolean success = false;
HttpURLConnection urlConn = null;
try {
URL url = new URL( urlStr );
urlConn = (HttpURLConnection)url.openConnection();
InputStream in = new
BufferedInputStream( urlConn.getInputStream(),
1024*8 );
success = DictUtils.saveDict( context, in,
name, loc );
DbgUtils.logf( "saveDict returned %b", success );
} catch ( java.net.MalformedURLException mue ) {
DbgUtils.loge( mue );
} catch ( java.io.IOException ioe ) {
DbgUtils.loge( ioe );
} finally {
if ( null != urlConn ) {
urlConn.disconnect();
}
}
sno.close();
DictLangCache.inval( context, name, loc, true );
if ( null != lstnr ) {
lstnr.downloadFinished( name, success );
}
}
} ).start();
return len;
}
}

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 )
{
@ -318,13 +318,14 @@ public class NewGameActivity extends XWActivity {
String inviteID = null;
long rowid;
int[] lang = {0};
String[] dict = {null};
final int nPlayers = 2; // hard-coded for no-configure case
if ( networked ) {
room = GameUtils.makeRandomID();
inviteID = GameUtils.makeRandomID();
rowid = GameUtils.makeNewNetGame( this, room, inviteID, lang,
nPlayers, 1 );
dict, nPlayers, 1 );
} else {
rowid = GameUtils.saveNew( this, new CurGameInfo( this ) );
}
@ -333,7 +334,8 @@ public class NewGameActivity extends XWActivity {
GameUtils.launchGame( this, rowid, networked );
if ( networked ) {
GameUtils.launchInviteActivity( this, choseEmail, room,
inviteID, lang[0], nPlayers );
inviteID, lang[0], dict[0],
nPlayers );
}
} else {
GameUtils.doConfig( this, rowid, GameConfig.class );
@ -355,7 +357,7 @@ public class NewGameActivity extends XWActivity {
intent.putExtra( GameUtils.INTENT_FORRESULT_ROWID, true );
startActivityForResult( intent, CONFIG_FOR_BT );
} else {
GameUtils.launchBTInviter( this, 1, INVITE_FOR_BT );
BTInviteActivity.launchForResult( this, 1, INVITE_FOR_BT );
}
}
@ -376,7 +378,7 @@ public class NewGameActivity extends XWActivity {
intent.putExtra( GameUtils.INTENT_FORRESULT_ROWID, true );
startActivityForResult( intent, CONFIG_FOR_SMS );
} else {
GameUtils.launchSMSInviter( this, 1, INVITE_FOR_SMS );
SMSInviteActivity.launchForResult( this, 1, INVITE_FOR_SMS );
}
}

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

@ -1,57 +0,0 @@
/* -*- compile-command: "cd ../../../../../; ant install"; -*- */
/*
* Copyright 2009-2010 by Eric House (xwords@eehouse.org). All
* rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.content.Context;
import java.util.HashMap;
import java.util.ArrayList;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.*;
public class RelayMsgSink extends MultiMsgSink {
private HashMap<String,ArrayList<byte[]>> m_msgLists = null;
public void send( Context context )
{
NetUtils.sendToRelay( context, m_msgLists );
}
/***** TransportProcs interface *****/
public boolean relayNoConnProc( byte[] buf, String relayID )
{
if ( null == m_msgLists ) {
m_msgLists = new HashMap<String,ArrayList<byte[]>>();
}
ArrayList<byte[]> list = m_msgLists.get( relayID );
if ( list == null ) {
list = new ArrayList<byte[]>();
m_msgLists.put( relayID, list );
}
list.add( buf );
return true;
}
}

View file

@ -24,18 +24,18 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import javax.net.SocketFactory;
import java.net.InetAddress;
import java.net.Socket;
import java.io.InputStream;
import java.io.DataInputStream;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import org.eehouse.android.xw4.jni.GameSummary;
public class RelayService extends Service {
private static final int MAX_SEND = 1024;
private static final int MAX_BUF = MAX_SEND - 2;
@Override
public void onCreate()
@ -63,62 +63,153 @@ public class RelayService extends Service {
long[] rowids = DBUtils.getRowIDsFor( this, relayID );
if ( null != rowids ) {
for ( long rowid : rowids ) {
Intent intent = new Intent( this, DispatchNotify.class );
intent.putExtra( DispatchNotify.RELAYIDS_EXTRA,
new String[] {relayID} );
Intent intent =
GamesList.makeRelayIdsIntent( this,
new String[] {relayID} );
String msg = Utils.format( this, R.string.notify_bodyf,
GameUtils.getName( this, rowid ) );
Utils.postNotification( this, intent, R.string.notify_title,
msg, relayID.hashCode() );
msg, (int)rowid );
}
}
}
}
private String[] collectIDs( int[] nBytes )
{
String[] ids = DBUtils.getRelayIDs( this, false );
int len = 0;
if ( null != ids ) {
for ( String id : ids ) {
len += id.length();
}
}
nBytes[0] = len;
return ids;
}
private void fetchAndProcess()
{
int[] nBytes = new int[1];
String[] ids = collectIDs( nBytes );
if ( null != ids && 0 < ids.length ) {
RelayMsgSink sink = new RelayMsgSink();
byte[][][] msgs =
NetUtils.queryRelay( this, ids, nBytes[0] );
long[][] rowIDss = new long[1][];
String[] relayIDs = DBUtils.getRelayIDs( this, rowIDss );
if ( null != relayIDs && 0 < relayIDs.length ) {
long[] rowIDs = rowIDss[0];
byte[][][] msgs = NetUtils.queryRelay( this, relayIDs );
if ( null != msgs ) {
int nameCount = ids.length;
RelayMsgSink sink = new RelayMsgSink();
int nameCount = relayIDs.length;
ArrayList<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 );
if ( !DispatchNotify.tryHandle( relayIDs ) ) {
setupNotification( relayIDs );
}
String[] tmp = new String[idsWMsgs.size()];
idsWMsgs.toArray( tmp );
setupNotification( tmp );
}
sink.send( this );
}
}
}
private static void sendToRelay( Context context,
HashMap<String,ArrayList<byte[]>> msgHash )
{
// format: total msg lenth: 2
// number-of-relayIDs: 2
// for-each-relayid: relayid + '\n': varies
// message count: 1
// for-each-message: length: 2
// message: varies
if ( null != msgHash ) {
try {
// Build up a buffer containing everything but the total
// message length and number of relayIDs in the message.
ByteArrayOutputStream store =
new ByteArrayOutputStream( MAX_BUF ); // mem
DataOutputStream outBuf = new DataOutputStream( store );
int msgLen = 4; // relayID count + protocol stuff
int nRelayIDs = 0;
Iterator<String> iter = msgHash.keySet().iterator();
while ( iter.hasNext() ) {
String relayID = iter.next();
int thisLen = 1 + relayID.length(); // string and '\n'
thisLen += 2; // message count
ArrayList<byte[]> msgs = msgHash.get( relayID );
for ( byte[] msg : msgs ) {
thisLen += 2 + msg.length;
}
if ( msgLen + thisLen > MAX_BUF ) {
// Need to deal with this case by sending multiple
// packets. It WILL happen.
break;
}
// got space; now write it
++nRelayIDs;
outBuf.writeBytes( relayID );
outBuf.write( '\n' );
outBuf.writeShort( msgs.size() );
for ( byte[] msg : msgs ) {
outBuf.writeShort( msg.length );
outBuf.write( msg );
}
msgLen += thisLen;
}
// Now open a real socket, write size and proto, and
// copy in the formatted buffer
Socket socket = NetUtils.makeProxySocket( context, 8000 );
if ( null != socket ) {
DataOutputStream outStream =
new DataOutputStream( socket.getOutputStream() );
outStream.writeShort( msgLen );
outStream.writeByte( NetUtils.PROTOCOL_VERSION );
outStream.writeByte( NetUtils.PRX_PUT_MSGS );
outStream.writeShort( nRelayIDs );
outStream.write( store.toByteArray() );
outStream.flush();
socket.close();
}
} catch ( java.io.IOException ioe ) {
DbgUtils.loge( ioe );
}
} else {
DbgUtils.logf( "sendToRelay: null msgs" );
}
} // sendToRelay
private class RelayMsgSink extends MultiMsgSink {
private HashMap<String,ArrayList<byte[]>> m_msgLists = null;
public void send( Context context )
{
sendToRelay( context, m_msgLists );
}
/***** TransportProcs interface *****/
public boolean relayNoConnProc( byte[] buf, String relayID )
{
if ( null == m_msgLists ) {
m_msgLists = new HashMap<String,ArrayList<byte[]>>();
}
ArrayList<byte[]> list = m_msgLists.get( relayID );
if ( list == null ) {
list = new ArrayList<byte[]>();
m_msgLists.put( relayID, list );
}
list.add( buf );
return true;
}
}
}

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 ) {
@ -388,7 +388,6 @@ public class SMSService extends Service {
int count = (msg.length() + (MAX_LEN_TEXT-1)) / MAX_LEN_TEXT;
String[] result = new String[count];
int msgID = ++s_nSent % 0x000000FF;
DbgUtils.logf( "preparing %d packets for msgid %x", count, msgID );
int start = 0;
int end = 0;
@ -400,7 +399,6 @@ public class SMSService extends Service {
end += len;
result[ii] = String.format( "0:%X:%X:%X:%s", msgID, ii, count,
msg.substring( start, end ) );
DbgUtils.logf( "fragment[%d]: %s", ii, result[ii] );
start = end;
}
return result;
@ -424,17 +422,16 @@ public class SMSService extends Service {
makeForInvite( phone, gameID, gameName, lang, dict,
nPlayersT, nPlayersH );
} else {
Intent intent = new Intent( this, DictsActivity.class );
fillInviteIntent( intent, phone, gameID, gameName, lang, dict,
nPlayersT, nPlayersH );
Intent intent = MultiService
.makeMissingDictIntent( this, gameName, lang, dict,
nPlayersT, nPlayersH );
intent.putExtra( PHONE, phone );
intent.putExtra( MultiService.OWNER,
MultiService.OWNER_SMS );
intent.putExtra( MultiService.INVITER,
Utils.phoneToContact( this, phone, true ) );
Utils.postNotification( this, intent,
R.string.missing_dict_title,
R.string.missing_dict_detail,
gameID );
MultiService.postMissingDictNotification( this, intent,
gameID );
}
break;
case DATA:
@ -506,7 +503,6 @@ public class SMSService extends Service {
private void disAssemble( String senderPhone, String fullMsg )
{
DbgUtils.logf( "disAssemble()" );
byte[] data = XwJNI.base64Decode( fullMsg );
DataInputStream dis =
new DataInputStream( new ByteArrayInputStream(data) );
@ -544,7 +540,7 @@ public class SMSService extends Service {
String owner = Utils.phoneToContact( this, phone, true );
String body = Utils.format( this, R.string.new_name_bodyf,
owner );
postNotification( gameID, R.string.new_sms_title, body );
postNotification( gameID, R.string.new_sms_title, body, rowid );
ackInvite( phone, gameID );
}
@ -565,8 +561,6 @@ public class SMSService extends Service {
for ( String fragment : fragments ) {
String asPublic = toPublicFmt( fragment );
mgr.sendTextMessage( phone, null, asPublic, sent, delivery );
DbgUtils.logf( "Message \"%s\" of %d bytes sent to %s.",
asPublic, asPublic.length(), phone );
}
if ( s_showToasts ) {
DbgUtils.showf( this, "sent %dth msg", s_nSent );
@ -591,11 +585,8 @@ public class SMSService extends Service {
{
intent.putExtra( PHONE, phone );
intent.putExtra( MultiService.GAMEID, gameID );
intent.putExtra( MultiService.GAMENAME, gameName );
intent.putExtra( MultiService.LANG, lang );
intent.putExtra( MultiService.DICT, dict );
intent.putExtra( MultiService.NPLAYERST, nPlayersT );
intent.putExtra( MultiService.NPLAYERSH, nPlayersH );
MultiService.fillInviteIntent( intent, gameName, lang, dict,
nPlayersT, nPlayersH );
}
private void feedMessage( int gameID, byte[] msg, CommsAddrRec addr )
@ -612,19 +603,19 @@ public class SMSService extends Service {
if ( GameUtils.feedMessage( this, rowid, msg, addr,
sink ) ) {
postNotification( gameID, R.string.new_smsmove_title,
getString(R.string.new_move_body)
);
getString(R.string.new_move_body),
rowid );
}
}
}
}
}
private void postNotification( int gameID, int title, String body )
private void postNotification( int gameID, int title, String body,
long rowid )
{
Intent intent = new Intent( this, DispatchNotify.class );
intent.putExtra( DispatchNotify.GAMEID_EXTRA, gameID );
Utils.postNotification( this, intent, title, body, gameID );
Intent intent = GamesList.makeGameIDIntent( this, gameID );
Utils.postNotification( this, intent, title, body, (int)rowid );
}
// Runs in separate thread
@ -669,11 +660,9 @@ public class SMSService extends Service {
@Override
public void onReceive(Context arg0, Intent arg1)
{
DbgUtils.logf( "got MSG_DELIVERED" );
switch ( getResultCode() ) {
case Activity.RESULT_OK:
sendResult( MultiEvent.SMS_SEND_OK );
DbgUtils.logf( "SUCCESS!!!" );
break;
case SmsManager.RESULT_ERROR_RADIO_OFF:
DbgUtils.showf( SMSService.this, "NO RADIO!!!" );
@ -693,7 +682,6 @@ public class SMSService extends Service {
@Override
public void onReceive(Context arg0, Intent arg1)
{
DbgUtils.logf( "got MSG_DELIVERED" );
if ( Activity.RESULT_OK == getResultCode() ) {
DbgUtils.logf( "SUCCESS!!!" );
} else {
@ -714,7 +702,6 @@ public class SMSService extends Service {
public int transportSend( byte[] buf, final CommsAddrRec addr, int gameID )
{
int nSent = -1;
DbgUtils.logf( "SMSMsgSink.transportSend()" );
if ( null != addr ) {
nSent = sendPacket( addr.sms_phone, gameID, buf );
} else {
@ -757,7 +744,6 @@ public class SMSService extends Service {
public boolean isComplete()
{
boolean complete = m_msgs.length == m_haveCount;
DbgUtils.logf( "isComplete(msg %d)=>%b", m_msgID, complete );
return complete;
}

View file

@ -1,58 +0,0 @@
/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
/*
* Copyright 2012 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
public class StatusNotifier {
private int m_id;
private NotificationManager m_mgr;
private Context m_context;
public StatusNotifier( Context context, String msg, int id )
{
m_context = context;
m_id = id;
Notification notification =
new Notification( R.drawable.icon48x48, msg,
System.currentTimeMillis() );
notification.flags = notification.flags |= Notification.FLAG_AUTO_CANCEL;
PendingIntent pi = PendingIntent.getActivity( context, 0,
new Intent(), 0 );
notification.setLatestEventInfo( context, "", "", pi );
m_mgr = (NotificationManager)
context.getSystemService( Context.NOTIFICATION_SERVICE );
m_mgr.notify( id, notification );
}
// Will likely be called from background thread
public void close()
{
m_mgr.cancel( m_id );
}
}

View file

@ -28,7 +28,9 @@ import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.SystemClock;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@ -153,68 +155,8 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
}
if ( 0 < params.length() ) {
HttpPost post = makePost( context, "getUpdates" );
String json = runPost( post, params );
makeNotificationsIf( context, fromUI, json, pm, packageName, dals );
}
}
private static void makeNotificationsIf( Context context, boolean fromUI,
String jstr, PackageManager pm,
String packageName,
DictUtils.DictAndLoc[] dals )
{
boolean gotOne = false;
try {
JSONObject jobj = new JSONObject( jstr );
if ( null != jobj ) {
if ( jobj.has( k_APP ) ) {
JSONObject app = jobj.getJSONObject( k_APP );
if ( app.has( k_URL ) ) {
String url = app.getString( k_URL );
ApplicationInfo ai = pm.getApplicationInfo( packageName, 0);
String label = pm.getApplicationLabel( ai ).toString();
Intent intent =
new Intent( Intent.ACTION_VIEW, Uri.parse(url) );
String title =
Utils.format( context, R.string.new_app_availf, label );
String body = context.getString( R.string.new_app_avail );
Utils.postNotification( context, intent, title, body,
url.hashCode() );
gotOne = true;
}
}
if ( jobj.has( k_DICTS ) ) {
JSONArray dicts = jobj.getJSONArray( k_DICTS );
for ( int ii = 0; ii < dicts.length(); ++ii ) {
JSONObject dict = dicts.getJSONObject( ii );
if ( dict.has( k_URL ) && dict.has( k_INDEX ) ) {
String url = dict.getString( k_URL );
int index = dict.getInt( k_INDEX );
DictUtils.DictAndLoc dal = dals[index];
Intent intent =
new Intent( context, DictsActivity.class );
intent.putExtra( NEW_DICT_URL, url );
intent.putExtra( NEW_DICT_LOC, dal.loc.ordinal() );
String body =
Utils.format( context, R.string.new_dict_availf,
dal.name );
Utils.postNotification( context, intent,
R.string.new_dict_avail,
body, url.hashCode() );
gotOne = true;
}
}
}
}
} catch ( org.json.JSONException jse ) {
DbgUtils.loge( jse );
} catch ( PackageManager.NameNotFoundException nnfe ) {
DbgUtils.loge( nnfe );
}
if ( !gotOne && fromUI ) {
Utils.showToast( context, R.string.checkupdates_none_found );
new UpdateQueryTask( context, params, fromUI, pm,
packageName, dals ).execute();
}
}
@ -278,4 +220,121 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
false );
}
private static class UpdateQueryTask extends AsyncTask<Void, Void, String> {
private Context m_context;
private JSONObject m_params;
private boolean m_fromUI;
private PackageManager m_pm;
private String m_packageName;
private DictUtils.DictAndLoc[] m_dals;
public UpdateQueryTask( Context context, JSONObject params,
boolean fromUI, PackageManager pm,
String packageName,
DictUtils.DictAndLoc[] dals )
{
m_context = context;
m_params = params;
m_fromUI = fromUI;
m_pm = pm;
m_packageName = packageName;
m_dals = dals;
}
@Override protected String doInBackground( Void... unused )
{
HttpPost post = makePost( m_context, "getUpdates" );
String json = runPost( post, m_params );
return json;
}
@Override protected void onPostExecute( String json )
{
if ( null != json ) {
makeNotificationsIf( json );
}
}
private void makeNotificationsIf( String jstr )
{
boolean gotOne = false;
try {
JSONObject jobj = new JSONObject( jstr );
if ( null != jobj ) {
if ( jobj.has( k_APP ) ) {
JSONObject app = jobj.getJSONObject( k_APP );
if ( app.has( k_URL ) ) {
ApplicationInfo ai =
m_pm.getApplicationInfo( m_packageName, 0);
String label = m_pm.getApplicationLabel( ai ).toString();
// If there's a download dir AND an installer
// app, handle this ourselves. Otherwise just
// launch the browser
boolean useBrowser;
File downloads = DictUtils.getDownloadDir( m_context );
if ( null == downloads ) {
useBrowser = true;
} else {
File tmp = new File( downloads,
"xx" + XWConstants.APK_EXTN );
useBrowser = !Utils.canInstall( m_context, tmp );
}
Intent intent;
String url = app.getString( k_URL );
if ( useBrowser ) {
intent = new Intent( Intent.ACTION_VIEW,
Uri.parse(url) );
} else {
intent = DictImportActivity
.makeAppDownloadIntent( m_context, url );
}
String title =
Utils.format( m_context, R.string.new_app_availf,
label );
String body =
m_context.getString( R.string.new_app_avail );
Utils.postNotification( m_context, intent, title,
body, url.hashCode() );
gotOne = true;
}
}
if ( jobj.has( k_DICTS ) ) {
JSONArray dicts = jobj.getJSONArray( k_DICTS );
for ( int ii = 0; ii < dicts.length(); ++ii ) {
JSONObject dict = dicts.getJSONObject( ii );
if ( dict.has( k_URL ) && dict.has( k_INDEX ) ) {
String url = dict.getString( k_URL );
int index = dict.getInt( k_INDEX );
DictUtils.DictAndLoc dal = m_dals[index];
Intent intent =
new Intent( m_context, DictsActivity.class );
intent.putExtra( NEW_DICT_URL, url );
intent.putExtra( NEW_DICT_LOC, dal.loc.ordinal() );
String body =
Utils.format( m_context,
R.string.new_dict_availf,
dal.name );
Utils.postNotification( m_context, intent,
R.string.new_dict_avail,
body, url.hashCode() );
gotOne = true;
}
}
}
}
} catch ( org.json.JSONException jse ) {
DbgUtils.loge( jse );
} catch ( PackageManager.NameNotFoundException nnfe ) {
DbgUtils.loge( nnfe );
}
if ( !gotOne && m_fromUI ) {
Utils.showToast( m_context, R.string.checkupdates_none_found );
}
}
}
}

View file

@ -32,19 +32,25 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.PhoneLookup;
import android.telephony.TelephonyManager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import junit.framework.Assert;
@ -60,6 +66,7 @@ public class Utils {
private static Boolean s_isFirstBootThisVersion = null;
private static Boolean s_deviceSupportSMS = null;
private static Boolean s_isFirstBootEver = null;
private static Integer s_appVersion = null;
private static HashMap<String,String> s_phonesHash =
new HashMap<String,String>();
private static int s_nextCode = 0; // keep PendingIntents unique
@ -169,7 +176,8 @@ public class Utils {
}
public static void postNotification( Context context, Intent intent,
String title, String body, int id )
String title, String body,
int id )
{
/* s_nextCode: per this link
http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager
@ -339,6 +347,12 @@ public class Utils {
}
}
public static void setItemVisible( Menu menu, int id, boolean enabled )
{
MenuItem item = menu.findItem( id );
item.setVisible( enabled );
}
public static boolean hasSmallScreen( Context context )
{
if ( null == s_hasSmallScreen ) {
@ -403,18 +417,49 @@ public class Utils {
return dict_url;
}
public static int getAppVersion( Context context )
{
if ( null == s_appVersion ) {
try {
int version = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0)
.versionCode;
s_appVersion = new Integer( version );
} catch ( Exception e ) {
DbgUtils.loge( e );
}
}
return null == s_appVersion? 0 : s_appVersion;
}
public static Intent makeInstallIntent( File file )
{
String withScheme = "file://" + file.getPath();
Uri uri = Uri.parse( withScheme );
Intent intent = new Intent( Intent.ACTION_VIEW );
intent.setDataAndType( uri, XWConstants.APK_TYPE );
intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
return intent;
}
// Return whether there's an app installed that can install
public static boolean canInstall( Context context, File path )
{
boolean result = false;
PackageManager pm = context.getPackageManager();
Intent intent = makeInstallIntent( path );
List<ResolveInfo> doers =
pm.queryIntentActivities( intent,
PackageManager.MATCH_DEFAULT_ONLY );
result = 0 < doers.size();
return result;
}
private static void setFirstBootStatics( Context context )
{
int thisVersion = 0;
int thisVersion = getAppVersion( context );
int prevVersion = 0;
try {
thisVersion = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0)
.versionCode;
} catch ( Exception e ) {
}
SharedPreferences prefs = null;
if ( 0 < thisVersion ) {
prefs = context.getSharedPreferences( HIDDEN_PREFS,

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;
@ -48,7 +48,6 @@ public class XWActivity extends Activity
{
DbgUtils.logf( "%s.onStart(this=%H)", getClass().getName(), this );
super.onStart();
DispatchNotify.SetRunning( this );
}
@Override
@ -73,7 +72,6 @@ public class XWActivity extends Activity
protected void onStop()
{
DbgUtils.logf( "%s.onStop(this=%H)", getClass().getName(), this );
DispatchNotify.ClearRunning( this );
super.onStop();
}
@ -194,7 +192,7 @@ public class XWActivity extends Activity
Assert.fail();
}
// BTService.BTEventListener interface
// BTService.MultiEventListener interface
public void eventOccurred( MultiService.MultiEvent event,
final Object ... args )
{

View file

@ -28,11 +28,14 @@ import java.util.UUID;
import org.eehouse.android.xw4.jni.XwJNI;
public class XWApp extends Application {
public static final boolean DEBUG_LOCKS = false;
public static final boolean BTSUPPORTED = false;
public static final boolean SMSSUPPORTED = true;
public static final boolean GCMSUPPORTED = true;
public static final boolean DEBUG = false;
public static final boolean ATTACH_SUPPORTED = true;
public static final boolean REMATCH_SUPPORTED = false;
public static final boolean DEBUG = true;
public static final boolean DEBUG_LOCKS = false && DEBUG;
public static final boolean DEBUG_EXP_TIMERS = false && DEBUG;
public static final String SMS_PUBLIC_HEADER = "-XW4";

View file

@ -23,4 +23,7 @@ package org.eehouse.android.xw4;
public interface XWConstants {
public static final String GAME_EXTN = ".xwg";
public static final String DICT_EXTN = ".xwd";
public static final String APK_EXTN = ".apk";
public static final String APK_TYPE =
"application/vnd.android.package-archive";
}

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;
@ -45,7 +45,6 @@ public class XWListActivity extends ListActivity
{
DbgUtils.logf( "%s.onStart(this=%H)", getClass().getName(), this );
super.onStart();
DispatchNotify.SetRunning( this );
}
@Override
@ -70,7 +69,6 @@ public class XWListActivity extends ListActivity
protected void onStop()
{
DbgUtils.logf( "%s.onStop(this=%H)", getClass().getName(), this );
DispatchNotify.ClearRunning( this );
super.onStop();
}
@ -195,7 +193,7 @@ public class XWListActivity extends ListActivity
m_delegate.launchLookup( words, lang, forceList );
}
// BTService.BTEventListener interface
// MultiService.MultiEventListener interface
public void eventOccurred( MultiService.MultiEvent event,
final Object ... args )
{

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

@ -24,6 +24,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import com.google.android.gcm.GCMRegistrar;
import java.util.ArrayList;
import java.util.ArrayList;
@ -44,11 +45,6 @@ public class XWPrefs {
return getPrefsString( context, R.string.key_relay_host );
}
public static String getDefaultRedirHost( Context context )
{
return getPrefsString( context, R.string.key_redir_host );
}
public static int getDefaultRelayPort( Context context )
{
String val = getPrefsString( context, R.string.key_relay_port );
@ -87,6 +83,11 @@ public class XWPrefs {
return getPrefsBoolean( context, R.string.key_ringer_zoom, false );
}
public static boolean getSquareTiles( Context context )
{
return getPrefsBoolean( context, R.string.key_square_tiles, false );
}
public static int getDefaultPlayerMinutes( Context context )
{
String value =
@ -112,6 +113,24 @@ public class XWPrefs {
return result;
}
public static int getPrefsInt( Context context, int keyID, int defaultValue )
{
String key = context.getString( keyID );
SharedPreferences sp = PreferenceManager
.getDefaultSharedPreferences( context );
return sp.getInt( key, defaultValue );
}
public static void setPrefsInt( Context context, int keyID, int newValue )
{
SharedPreferences sp = PreferenceManager
.getDefaultSharedPreferences( context );
SharedPreferences.Editor editor = sp.edit();
String key = context.getString( keyID );
editor.putInt( key, newValue );
editor.commit();
}
public static boolean getPrefsBoolean( Context context, int keyID,
boolean defaultValue )
{
@ -132,6 +151,25 @@ public class XWPrefs {
editor.commit();
}
public static long getPrefsLong( Context context, int keyID,
long defaultValue )
{
String key = context.getString( keyID );
SharedPreferences sp = PreferenceManager
.getDefaultSharedPreferences( context );
return sp.getLong( key, defaultValue );
}
public static void setPrefsLong( Context context, int keyID, long newVal )
{
SharedPreferences sp = PreferenceManager
.getDefaultSharedPreferences( context );
SharedPreferences.Editor editor = sp.edit();
String key = context.getString( keyID );
editor.putLong( key, newVal );
editor.commit();
}
public static void setClosedLangs( Context context, String[] langs )
{
setPrefsString( context, R.string.key_closed_langs,
@ -186,17 +224,27 @@ public class XWPrefs {
public static void setGCMDevID( Context context, String devID )
{
setPrefsString( context, R.string.key_gcm_regid, devID );
int curVers = Utils.getAppVersion( context );
setPrefsInt( context, R.string.key_gcmvers_regid, curVers );
clearPrefsKey( context, R.string.key_relay_regid );
}
public static String getGCMDevID( Context context )
{
return getPrefsString( context, R.string.key_gcm_regid );
int curVers = Utils.getAppVersion( context );
int storedVers = getPrefsInt( context, R.string.key_gcmvers_regid, 0 );
String result;
if ( 0 != storedVers && storedVers < curVers ) {
result = ""; // Don't trust what registrar has
} else {
result = GCMRegistrar.getRegistrationId( context );
}
return result;
}
public static void clearGCMDevID( Context context )
{
clearPrefsKey( context, R.string.key_gcm_regid );
clearRelayDevID( context );
}
public static String getRelayDevID( Context context )
@ -213,6 +261,11 @@ public class XWPrefs {
setPrefsString( context, R.string.key_relay_regid, idRelay );
}
public static void clearRelayDevID( Context context )
{
clearPrefsKey( context, R.string.key_relay_regid );
}
public static boolean getHaveCheckedSMS( Context context )
{
return getPrefsBoolean( context, R.string.key_checked_sms, false );
@ -241,6 +294,17 @@ public class XWPrefs {
return getPrefsBoolean( context, R.string.key_default_loc, true );
}
public static long getDefaultNewGameGroup( Context context )
{
return getPrefsLong( context, R.string.key_default_group,
DBUtils.ROWID_NOTFOUND );
}
public static void setDefaultNewGameGroup( Context context, long val )
{
setPrefsLong( context, R.string.key_default_group, val );
}
protected static String getPrefsString( Context context, int keyID )
{
String key = context.getString( keyID );

View file

@ -22,18 +22,20 @@
package org.eehouse.android.xw4.jni;
import android.content.Context;
import java.lang.InterruptedException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.Iterator;
import android.os.Handler;
import android.os.Message;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import java.lang.InterruptedException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.concurrent.LinkedBlockingQueue;
import org.eehouse.android.xw4.R;
import org.eehouse.android.xw4.DbgUtils;
import org.eehouse.android.xw4.ConnStatusHandler;
import org.eehouse.android.xw4.BoardDims;
import org.eehouse.android.xw4.GameLock;
import org.eehouse.android.xw4.GameUtils;
import org.eehouse.android.xw4.DBUtils;
import org.eehouse.android.xw4.Toolbar;
@ -77,7 +79,7 @@ public class JNIThread extends Thread {
CMD_COUNTS_VALUES,
CMD_REMAINING,
CMD_RESEND,
CMD_ACKANY,
// CMD_ACKANY,
CMD_HISTORY,
CMD_FINAL,
CMD_ENDGAME,
@ -94,6 +96,7 @@ public class JNIThread extends Thread {
public static final int QUERY_ENDGAME = 4;
public static final int TOOLBAR_STATES = 5;
public static final int GOT_WORDS = 6;
public static final int GAME_OVER = 7;
public class GameStateInfo implements Cloneable {
public int visTileCount;
@ -120,7 +123,8 @@ public class JNIThread extends Thread {
private boolean m_stopped = false;
private boolean m_saveOnStop = false;
private int m_jniGamePtr;
private GameUtils.GameLock m_lock;
private byte[] m_gameAtStart;
private GameLock m_lock;
private Context m_context;
private CurGameInfo m_gi;
private Handler m_handler;
@ -141,10 +145,12 @@ public class JNIThread extends Thread {
Object[] m_args;
}
public JNIThread( int gamePtr, CurGameInfo gi, SyncedDraw drawer,
GameUtils.GameLock lock, Context context, Handler handler )
public JNIThread( int gamePtr, byte[] gameAtStart, CurGameInfo gi,
SyncedDraw drawer, GameLock lock, Context context,
Handler handler )
{
m_jniGamePtr = gamePtr;
m_gameAtStart = gameAtStart;
m_gi = gi;
m_drawer = drawer;
m_lock = lock;
@ -284,13 +290,17 @@ public class JNIThread extends Thread {
if ( null != m_newDict ) {
m_gi.dictName = m_newDict;
}
GameSummary summary = new GameSummary( m_context, m_gi );
XwJNI.game_summarize( m_jniGamePtr, summary );
byte[] state = XwJNI.game_saveToStream( m_jniGamePtr, m_gi );
GameUtils.saveGame( m_context, state, m_lock, false );
DBUtils.saveSummary( m_context, m_lock, summary );
// There'd better be no way for saveGame above to fail!
XwJNI.game_saveSucceeded( m_jniGamePtr );
if ( Arrays.equals( m_gameAtStart, state ) ) {
DbgUtils.logf( "no change in game; can skip saving" );
} else {
GameSummary summary = new GameSummary( m_context, m_gi );
XwJNI.game_summarize( m_jniGamePtr, summary );
DBUtils.saveGame( m_context, m_lock, state, false );
DBUtils.saveSummary( m_context, m_lock, summary );
// There'd better be no way for saveGame above to fail!
XwJNI.game_saveSucceeded( m_jniGamePtr );
}
}
@SuppressWarnings("fallthrough")
@ -495,11 +505,12 @@ public class JNIThread extends Thread {
case CMD_RESEND:
XwJNI.comms_resendAll( m_jniGamePtr,
((Boolean)args[0]).booleanValue() );
break;
case CMD_ACKANY:
XwJNI.comms_ackAny( m_jniGamePtr );
((Boolean)args[0]).booleanValue(),
((Boolean)args[1]).booleanValue() );
break;
// case CMD_ACKANY:
// XwJNI.comms_ackAny( m_jniGamePtr );
// break;
case CMD_HISTORY:
boolean gameOver = XwJNI.server_getGameIsOver( m_jniGamePtr );
@ -523,8 +534,14 @@ public class JNIThread extends Thread {
case CMD_POST_OVER:
if ( XwJNI.server_getGameIsOver( m_jniGamePtr ) ) {
sendForDialog( R.string.finalscores_title,
XwJNI.server_writeFinalScores( m_jniGamePtr ) );
boolean auto = 0 < args.length &&
((Boolean)args[0]).booleanValue();
int titleID = auto? R.string.summary_gameover
: R.string.finalscores_title;
String text = XwJNI.server_writeFinalScores( m_jniGamePtr );
Message.obtain( m_handler, GAME_OVER, titleID, 0, text )
.sendToTarget();
}
break;

View file

@ -57,11 +57,12 @@ public interface UtilCtxt {
void setIsServer( boolean isServer );
// Possible values for typ[0], these must match enum in xwrelay.sh
public static final int ID_TYPE_NONE = 0;
public static final int ID_TYPE_RELAY = 1;
public static final int ID_TYPE_ANDROID_GCM = 3;
String getDevID( /*out*/ byte[] typ );
void deviceRegistered( String idRelay );
void deviceRegistered( int devIDType, String idRelay );
void bonusSquareHeld( int bonus );
void playerScoreHeld( int player );

View file

@ -94,21 +94,37 @@ public class UtilCtxtImpl implements UtilCtxt {
subclassOverride( "setIsServer" );
}
public String getDevID( /*out*/ byte[] typ )
public String getDevID( /*out*/ byte[] typa )
{
byte typ = UtilCtxt.ID_TYPE_NONE;
String result = XWPrefs.getRelayDevID( m_context );
if ( null != result ) {
typ[0] = UtilCtxt.ID_TYPE_RELAY;
typ = UtilCtxt.ID_TYPE_RELAY;
} else {
result = XWPrefs.getGCMDevID( m_context );
typ[0] = UtilCtxt.ID_TYPE_ANDROID_GCM;
if ( result.equals("") ) {
result = null;
} else {
typ = UtilCtxt.ID_TYPE_ANDROID_GCM;
}
}
typa[0] = typ;
return result;
}
public void deviceRegistered( String idRelay )
public void deviceRegistered( int devIDType, String idRelay )
{
XWPrefs.setRelayDevID( m_context, idRelay );
switch ( devIDType ) {
case UtilCtxt.ID_TYPE_RELAY:
XWPrefs.setRelayDevID( m_context, idRelay );
break;
case UtilCtxt.ID_TYPE_NONE:
XWPrefs.clearRelayDevID( m_context );
break;
default:
Assert.fail();
break;
}
}
public void bonusSquareHeld( int bonus )

View file

@ -238,7 +238,8 @@ public class XwJNI {
public static native void comms_getAddr( int gamePtr, CommsAddrRec addr );
public static native CommsAddrRec[] comms_getAddrs( int gamePtr );
public static native void comms_setAddr( int gamePtr, CommsAddrRec addr );
public static native void comms_resendAll( int gamePtr, boolean andAck );
public static native void comms_resendAll( int gamePtr, boolean force,
boolean andAck );
public static native void comms_ackAny( int gamePtr );
public static native void comms_transportFailed( int gamePtr );
public static native boolean comms_isConnected( int gamePtr );

View file

@ -0,0 +1,112 @@
<?php
$g_androidStrings = array( "android", );
$g_apk = 'XWords4-release_android_beta_55-39-gbffb231.apk';
function printHead() {
print <<<EOF
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<link rel="stylesheet" type="text/css" href="/xw4mobile.css" />
<title>Crosswords Invite redirect</title>
</head>
<body>
<div class="center">
<img class="center" src="../icon48x48.png"/>
</div>
EOF;
}
function printTail() {
print <<<EOF
</body>
</html>
EOF;
}
function printNonAndroid($agent) {
$subject = "Android device not identified";
$body = htmlentities("My browser is running on an android device but"
. " says its user agent is: \"$agent\"."
. " Please fix your website to recognize"
. " this as an Android browser.");
print <<<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>
</div>
EOF;
}
function printAndroid() {
print <<<EOF
<div>
<p>You&apos;ll have come here after clicking a link in an email or
text inviting you to a Crosswords game. But you should not be seeing
this page.</p>
<p>If you got this page on your device, it means either
<ul>
<li>The copy of Crosswords you have is NOT beta 56 or newer (dating from about Dec. 1, 2012).</li>
<li> OR </li>
<li> that your copy of Crosswords is new enough <em>BUT</em> that
when you clicked on the link and were asked to choose between a
browser and Crosswords you chose the browser.</li>
</ul></p>
<p>In the first case, install the latest Crosswords,
either <a href="market://search?q=pname:org.eehouse.android.xw4">via
the Google Play store</a> or
(sideloading) <a href="https://sourceforge.net/projects/xwords/files/xwords_Android/4.4%20beta%2056/XWords4-release_android_beta_56.apk/download">via
Sourceforge.net</a>. After the install is finished go back to the
invite email (or text) and tap the link again.</p>
<p>In the second case, hit your browser&apos;s back button, click the
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>
<div class="center">
<img class="center" src="../icon48x48.png"/>
</div>
EOF;
}
/**********************************************************************
* Main()
**********************************************************************/
$agent = $_SERVER['HTTP_USER_AGENT'];
$onAndroid = false;
for ( $ii = 0; $ii < count($g_androidStrings) && !$onAndroid; ++$ii ) {
$needle = $g_androidStrings[$ii];
$onAndroid = false !== stripos( $agent, $needle );
}
$onFire = false !== stripos( $agent, 'silk' );
printHead();
if ( /*true || */ $onFire || $onAndroid ) {
printAndroid();
} else {
printNonAndroid($agent);
}
printTail();
?>

View file

@ -1,8 +1,11 @@
#!/bin/sh
set -e -u
GCM_SENDER_ID=${GCM_SENDER_ID:-""}
if [ -z "$GCM_SENDER_ID" ]; then
echo "GCM_SENDER_ID not in env"
exit 1
echo "GCM_SENDER_ID empty; GCM use will be disabled" >&2
fi
cat <<EOF

View file

@ -1,8 +1,32 @@
<!-- -*- mode: sgml; -*- -->
<?php
// script to work around URLs with custom schemes not being clickable in
// Android's SMS app. It runs on my server and SMS messages hold links to it
// that it then redirects to the passed-in scheme.
function langToString( $code ) {
switch ( $code ) {
case 1: return "English";
case 2: return "French";
case 3: return "German";
case 4: return "Turkish";
case 5: return "Arabic";
case 6: return "Spanish";
case 7: return "Swedish";
case 8: return "Polish";
case 9: return "Danish";
case 0xA: return "Italian";
case 0xB: return "Dutch";
case 0xC: return "Catalan";
case 0xD: return "Portuguese";
case 0XF: return "Russian";
case 0x11: return "Czech";
case 0x12: return "Greek";
case 0x13: return "Slovak";
default:
return "<unknown>";
}
}
$g_androidStrings = array( "android", );
$scheme = "newxwgame";
$host = "10.0.2.2";
@ -10,33 +34,46 @@ $lang = $_REQUEST["lang"];
$room = $_REQUEST["room"];
$np = $_REQUEST["np"];
$id = $_REQUEST["id"];
$wl = $_REQUEST["wl"];
$content = "0; url=$scheme://$host?room=$room&lang=$lang&np=$np";
$agent = $_SERVER['HTTP_USER_AGENT'];
$onAndroid = false;
for ( $ii = 0; $ii < count($g_androidStrings) && !$onAndroid; ++$ii ) {
$needle = $g_androidStrings[$ii];
$onAndroid = 0 != stripos( $agent, $needle );
}
$onFire = 0 != stripos( $agent, 'silk' );
$localurl = "$scheme://$host?room=$room&lang=$lang&np=$np";
if ( $id != "" ) {
$content .= "&id=$id";
$localurl .= "&id=$id";
}
if ( $wl != "" ) {
$localurl .= "&wl=$wl";
}
if ( $onAndroid || $onFire ) {
print <<<EOF
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<link rel="stylesheet" type="text/css" href="/xw4mobile.css" />
<title>Crosswords SMS redirect</title>
<meta http-equiv="REFRESH"
content="$content">
<title>Crosswords Invite redirect</title>
</head>
<body>
<div align="center">
<img src="./icon48x48.png">
<img src="./icon48x48.png"/>
<p>redirecting to Crosswords....</p>
<p>This page is meant to be viewed (briefly) on your Android device after which Crosswords should launch.
If this fails it's probably because you don't have a new enough version of Crosswords installed.
<h1><a href="$localurl">Tap this link to launch Crosswords with
your new game.</a>
</h1>
<p>If this fails it&apos;s probably because you don&apos;t have a new enough
version of Crosswords installed.
</p>
<img src="./icon48x48.png"/>
</div>
</body>
@ -44,4 +81,77 @@ print <<<EOF
EOF;
} else if ( $onFire ) {
$langString = langToString($lang);
$langText = "Make sure the language chosen is $langString";
if ( '' != $wl ) {
$langText .= " and the wordlist is $wl.";
}
$langText .= " If you don't have a[n] $langString wordlist installed you'll need to do that first.";
print <<<EOF
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Crosswords Invite redirect</title>
</head>
<body>
<p>It appears you&apos;re running on a Kindle Fire, whose non-standard (from
an Android perspective) OS doesn't support the custom schemes on which
Crosswords invitations depend. If you want to accept this invitation
you'll need to do it the manual way:
<ol>
<li>Open Crosswords, and navigate to the main Games List screen</li>
<li>Choose &quot;Add game&quot;, either from the menu or the button at the bottom.</li>
<li>Under &quot;New Networked game&quot;, choose &quot;Configure first&quot;.</li>
<li>$langText</li>
<li>As the room name, enter &quot;$room&quot;.</li>
<li>Make sure the total number of players shown is $np and that only one of them is not an &quot;Off-device player&quot;.</li>
<li>Now tap the &quot;Play game&quot; button at the bottom (above the keyboard). Your new game should open and connect.</li>
</ol></p>
<p>I&apos;m sorry this is so complicated. I&apos;m trying to find a
workaround for this limitation in the Kindle Fire's operating system
but for now this is all I can offer.</p>
<p>(Just in case Amazon&apos;s fixed the
problem, <a href="$localurl">here is the link</a> that should open
your new game.)</p>
</body>
</html>
EOF;
} else {
$subject = "Android device not identified";
$body = htmlentities("My browser is running on an android device but"
. " says its user agent is: \"$agent\". Please fix your script to recognize"
. " this as an Android browser.");
print <<<EOF
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Crosswords Invite redirect</title>
</head>
<body>
<div align="center">
<img src="./icon48x48.png"/>
</div>
<p>This page is meant to be viewed on a browser on your Android
device. Please open the email that sent you here on that device and
revisit this link to complete the invitation process.
</p>
<p>(If you <em>are</em> viewing this on an Android device, you'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>
</body>
</html>
EOF;
}
?>

View file

@ -1,2 +1,3 @@
body { font-size: 2em; }
table { font-size: 2em; }
body { font-size: 1.5em; }
table { font-size: 1.5em; }
.center { text-align: center; }

View file

@ -1,6 +1,6 @@
/* -*- compile-command: "cd ../linux && make MEMDEBUG=TRUE -j3"; -*- */
/*
* Copyright 2001-2011 by Eric House (xwords@eehouse.org). All rights
* Copyright 2001 - 2012 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
@ -55,7 +55,9 @@ typedef struct MsgQueueElem {
XP_U8* msg;
XP_U16 len;
XP_PlayerAddr channelNo;
#ifdef DEBUG
XP_U16 sendCount; /* how many times sent? */
#endif
MsgID msgID; /* saved for ease of deletion */
#ifdef COMMS_CHECKSUM
gchar* checksum;
@ -108,6 +110,9 @@ struct CommsCtxt {
XP_U16 queueLen;
XP_U16 channelSeed; /* tries to be unique per device to aid
dupe elimination at start */
XP_U32 nextResend;
XP_U16 resendBackoff;
#ifdef COMMS_HEARTBEAT
XP_Bool doHeartbeat;
XP_U32 lastMsgRcvdTime;
@ -572,6 +577,10 @@ comms_makeFromStream( MPFORMAL XWStreamCtxt* stream, XW_UtilCtxt* util,
comms->channelSeed = stream_getU16( stream );
XP_LOGF( "%s: loaded seed: %.4X", __func__, comms->channelSeed );
}
if ( STREAM_VERS_COMMSBACKOFF <= version ) {
comms->resendBackoff = stream_getU16( stream );
comms->nextResend = stream_getU32( stream );
}
if ( addr.conType == COMMS_CONN_RELAY ) {
comms->r.myHostID = stream_getU8( stream );
stringFromStreamHere( stream, comms->r.connName,
@ -607,7 +616,7 @@ comms_makeFromStream( MPFORMAL XWStreamCtxt* stream, XW_UtilCtxt* util,
msg->channelNo = stream_getU16( stream );
msg->msgID = stream_getU32( stream );
#ifdef COMMS_HEARTBEAT
#ifdef DEBUG
msg->sendCount = 0;
#endif
msg->len = stream_getU16( stream );
@ -672,7 +681,7 @@ sendConnect( CommsCtxt* comms, XP_Bool breakExisting )
case COMMS_CONN_IP_DIRECT:
/* This will only work on host side when there's a single guest! */
(void)send_via_bt_or_ip( comms, BTIPMSG_RESET, CHANNEL_NONE, NULL, 0 );
(void)comms_resendAll( comms );
(void)comms_resendAll( comms, XP_FALSE );
break;
#endif
default:
@ -743,6 +752,8 @@ comms_writeToStream( CommsCtxt* comms, XWStreamCtxt* stream,
stream_putU32( stream, comms->connID );
stream_putU16( stream, comms->nextChannelNo );
stream_putU16( stream, comms->channelSeed );
stream_putU16( stream, comms->resendBackoff );
stream_putU32( stream, comms->nextResend );
if ( comms->addr.conType == COMMS_CONN_RELAY ) {
stream_putU8( stream, comms->r.myHostID );
stringToStream( stream, comms->r.connName );
@ -779,15 +790,25 @@ comms_writeToStream( CommsCtxt* comms, XWStreamCtxt* stream,
comms->lastSaveToken = saveToken;
} /* comms_writeToStream */
static void
resetBackoff( CommsCtxt* comms )
{
XP_LOGF( "%s: resetting backoff", __func__ );
comms->resendBackoff = 0;
comms->nextResend = 0;
}
void
comms_saveSucceeded( CommsCtxt* comms, XP_U16 saveToken )
{
XP_LOGF( "%s(saveToken=%d)", __func__, saveToken );
XP_ASSERT( !!comms );
if ( saveToken == comms->lastSaveToken ) {
XP_LOGF( "%s: lastSave matches", __func__ );
AddressRecord* rec;
for ( rec = comms->recs; !!rec; rec = rec->next ) {
XP_LOGF( "%s: lastSave matches; updating lastMsgSaved (%ld) to "
"lastMsgRcd (%ld)", __func__, rec->lastMsgSaved,
rec->lastMsgRcd );
rec->lastMsgSaved = rec->lastMsgRcd;
}
#ifdef XWFEATURE_COMMSACK
@ -942,7 +963,7 @@ makeElemWithID( CommsCtxt* comms, MsgID msgID, AddressRecord* rec,
sizeof( *newMsgElem ) );
newMsgElem->channelNo = channelNo;
newMsgElem->msgID = msgID;
#ifdef COMMS_HEARTBEAT
#ifdef DEBUG
newMsgElem->sendCount = 0;
#endif
@ -996,24 +1017,28 @@ comms_getChannelSeed( CommsCtxt* comms )
XP_S16
comms_send( CommsCtxt* comms, XWStreamCtxt* stream )
{
XP_PlayerAddr channelNo = stream_getAddress( stream );
XP_LOGF( "%s: channelNo=%x", __func__, channelNo );
AddressRecord* rec = getRecordFor( comms, NULL, channelNo, XP_FALSE );
MsgID msgID = (!!rec)? ++rec->nextMsgID : 0;
MsgQueueElem* elem;
XP_S16 result = -1;
if ( 0 == stream_getSize(stream) ) {
XP_LOGF( "%s: dropping 0-len message", __func__ );
} else {
XP_PlayerAddr channelNo = stream_getAddress( stream );
XP_LOGF( "%s: channelNo=%x", __func__, channelNo );
AddressRecord* rec = getRecordFor( comms, NULL, channelNo, XP_FALSE );
MsgID msgID = (!!rec)? ++rec->nextMsgID : 0;
MsgQueueElem* elem;
if ( 0 == channelNo ) {
channelNo = comms_getChannelSeed(comms) & ~CHANNEL_MASK;
}
if ( 0 == channelNo ) {
channelNo = comms_getChannelSeed(comms) & ~CHANNEL_MASK;
}
XP_DEBUGF( "%s: assigning msgID=" XP_LD " on chnl %x", __func__,
msgID, channelNo );
XP_DEBUGF( "%s: assigning msgID=" XP_LD " on chnl %x", __func__,
msgID, channelNo );
elem = makeElemWithID( comms, msgID, rec, channelNo, stream );
if ( NULL != elem ) {
addToQueue( comms, elem );
result = sendMsg( comms, elem );
elem = makeElemWithID( comms, msgID, rec, channelNo, stream );
if ( NULL != elem ) {
addToQueue( comms, elem );
result = sendMsg( comms, elem );
}
}
return result;
} /* comms_send */
@ -1037,9 +1062,10 @@ addToQueue( CommsCtxt* comms, MsgQueueElem* newMsgElem )
XP_ASSERT( comms->queueLen > 0 );
}
++comms->queueLen;
XP_LOGF( "%s: queueLen now %d after channelNo: %d; msgID: " XP_LD,
__func__, comms->queueLen,
newMsgElem->channelNo & CHANNEL_MASK, newMsgElem->msgID );
XP_LOGF( "%s: queueLen now %d after channelNo: %d; msgID: " XP_LD
"; len: %d", __func__, comms->queueLen,
newMsgElem->channelNo & CHANNEL_MASK, newMsgElem->msgID,
newMsgElem->len );
} /* addToQueue */
#ifdef DEBUG
@ -1207,8 +1233,11 @@ sendMsg( CommsCtxt* comms, MsgQueueElem* elem )
}
if ( result == elem->len ) {
#ifdef DEBUG
++elem->sendCount;
XP_LOGF( "%s: elem's sendCount now %d", __func__, elem->sendCount );
#endif
XP_LOGF( "%s: elem's sendCount since load: %d", __func__,
elem->sendCount );
}
XP_LOGF( "%s(channelNo=%d;msgID=" XP_LD ")=>%d", __func__,
@ -1224,17 +1253,32 @@ send_ack( CommsCtxt* comms )
}
XP_Bool
comms_resendAll( CommsCtxt* comms )
comms_resendAll( CommsCtxt* comms, XP_Bool force )
{
XP_Bool success = XP_TRUE;
MsgQueueElem* msg;
XP_ASSERT( !!comms );
for ( msg = comms->msgQueueHead; !!msg; msg = msg->next ) {
if ( 0 > sendMsg( comms, msg ) ) {
success = XP_FALSE;
break;
XP_U32 now = util_getCurSeconds( comms->util );
if ( !force && (now < comms->nextResend) ) {
XP_LOGF( "%s: aborting: %ld seconds left in backoff", __func__,
comms->nextResend - now );
success = XP_FALSE;
} else if ( !!comms->msgQueueHead ) {
MsgQueueElem* msg;
for ( msg = comms->msgQueueHead; !!msg; msg = msg->next ) {
if ( 0 > sendMsg( comms, msg ) ) {
success = XP_FALSE;
break;
}
}
/* Now set resend values */
if ( success && !force ) {
comms->resendBackoff = 2 * (1 + comms->resendBackoff);
XP_LOGF( "%s: backoff now %d", __func__, comms->resendBackoff );
comms->nextResend = now + comms->resendBackoff;
}
}
return success;
@ -1244,14 +1288,25 @@ comms_resendAll( CommsCtxt* comms )
void
comms_ackAny( CommsCtxt* comms )
{
#ifdef DEBUG
XP_Bool noneSent = XP_TRUE;
#endif
AddressRecord* rec;
for ( rec = comms->recs; !!rec; rec = rec->next ) {
if ( rec->lastMsgAckd < rec->lastMsgRcd ) {
XP_LOGF( "%s: %ld < %ld: rec needs ack", __func__,
rec->lastMsgAckd, rec->lastMsgRcd );
#ifdef DEBUG
noneSent = XP_FALSE;
#endif
XP_LOGF( "%s: channel %x; %ld < %ld: rec needs ack", __func__,
rec->channelNo, rec->lastMsgAckd, rec->lastMsgRcd );
sendEmptyMsg( comms, rec );
}
}
#ifdef DEBUG
if ( noneSent ) {
XP_LOGF( "%s: nothing to send", __func__ );
}
#endif
}
#endif
@ -1331,12 +1386,14 @@ got_connect_cmd( CommsCtxt* comms, XWStreamCtxt* stream,
#endif
#ifdef XWFEATURE_DEVID
if ( !reconnected ) {
XP_UCHAR devID[MAX_DEVID_LEN + 1];
DevIDType typ = stream_getU8( stream );
XP_UCHAR devID[MAX_DEVID_LEN + 1] = {0};
if ( ID_TYPE_NONE != typ ) {
stringFromStreamHere( stream, devID, sizeof(devID) );
if ( devID[0] != '\0' ) {
util_deviceRegistered( comms->util, devID );
}
}
if ( ID_TYPE_NONE == typ /* error case */
|| '\0' != devID[0] ) /* new info case */ {
util_deviceRegistered( comms->util, typ, devID );
}
#endif
@ -1366,7 +1423,7 @@ relayPreProcess( CommsCtxt* comms, XWStreamCtxt* stream, XWHostID* senderID )
break;
case XWRELAY_RECONNECT_RESP:
got_connect_cmd( comms, stream, XP_TRUE );
comms_resendAll( comms );
comms_resendAll( comms, XP_FALSE );
break;
case XWRELAY_ALLHERE:
@ -1404,7 +1461,7 @@ relayPreProcess( CommsCtxt* comms, XWStreamCtxt* stream, XWHostID* senderID )
on RECONNECTED, so removing the test for now to fix recon
problems on android. */
/* if ( COMMS_RELAYSTATE_RECONNECTED != comms->r.relayState ) { */
comms_resendAll( comms );
comms_resendAll( comms, XP_FALSE );
/* } */
if ( XWRELAY_ALLHERE == cmd ) { /* initial connect? */
(*comms->procs.rconnd)( comms->procs.closure,
@ -1513,7 +1570,7 @@ btIpPreProcess( CommsCtxt* comms, XWStreamCtxt* stream )
if ( consumed ) {
/* This is all there is so far */
if ( typ == BTIPMSG_RESET ) {
(void)comms_resendAll( comms );
(void)comms_resendAll( comms, XP_FALSE );
} else if ( typ == BTIPMSG_HB ) {
/* noteHBReceived( comms, addr ); */
} else {
@ -1681,6 +1738,7 @@ validateInitialMessage( CommsCtxt* comms,
rec = getRecordFor( comms, addr, *channelNo, XP_TRUE );
if ( !!rec ) {
/* reject: we've already seen init message on channel */
XP_LOGF( "%s: rejecting duplicate INIT message", __func__ );
rec = NULL;
} else {
if ( comms->isServer ) {
@ -1779,6 +1837,7 @@ comms_checkIncomingStream( CommsCtxt* comms, XWStreamCtxt* stream,
comms->lastSaveToken = 0; /* lastMsgRcd no longer valid */
stream_setAddress( stream, channelNo );
messageValid = payloadSize > 0;
resetBackoff( comms );
}
} else {
XP_LOGF( "%s: message too small", __func__ );
@ -1843,7 +1902,7 @@ sendEmptyMsg( CommsCtxt* comms, AddressRecord* rec )
0 /*rec? rec->lastMsgRcd : 0*/,
rec,
rec? rec->channelNo : 0, NULL );
sendMsg( comms, elem );
(void)sendMsg( comms, elem );
freeElem( comms, elem );
} /* sendEmptyMsg */
#endif
@ -2157,6 +2216,7 @@ msg_to_stream( CommsCtxt* comms, XWRELAY_Cmd cmd, XWHostID destID,
stream_putU16( stream, comms_getChannelSeed(comms) );
stream_putU8( stream, comms->util->gameInfo->dictLang );
stringToStream( stream, comms->r.connName );
putDevID( comms, stream );
set_relay_state( comms, COMMS_RELAYSTATE_CONNECT_PENDING );
break;
@ -2240,7 +2300,7 @@ sendNoConn( CommsCtxt* comms, const MsgQueueElem* elem, XWHostID destID )
}
}
LOG_RETURNF( "%d", success );
LOG_RETURNF( "%s", success?"TRUE":"FALSE" );
return success;
}

View file

@ -203,7 +203,7 @@ void comms_writeToStream( CommsCtxt* comms, XWStreamCtxt* stream,
void comms_saveSucceeded( CommsCtxt* comms, XP_U16 saveToken );
XP_S16 comms_send( CommsCtxt* comms, XWStreamCtxt* stream );
XP_Bool comms_resendAll( CommsCtxt* comms );
XP_Bool comms_resendAll( CommsCtxt* comms, XP_Bool force );
XP_U16 comms_getChannelSeed( CommsCtxt* comms );
#ifdef XWFEATURE_COMMSACK

View file

@ -47,6 +47,7 @@
#endif
#define MAX_COLS MAX_ROWS
#define STREAM_VERS_COMMSBACKOFF 0x16
#define STREAM_VERS_DICTNAME 0x15
#ifdef HASH_STREAM
# define STREAM_VERS_HASHSTREAM 0x14
@ -82,7 +83,7 @@
#define STREAM_VERS_41B4 0x02
#define STREAM_VERS_405 0x01
#define CUR_STREAM_VERS STREAM_VERS_DICTNAME
#define CUR_STREAM_VERS STREAM_VERS_COMMSBACKOFF
typedef struct XP_Rect {
XP_S16 left;

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 );
@ -473,6 +477,7 @@ gi_readFromStream( MPFORMAL XWStreamCtxt* stream, CurGameInfo* gi )
gi->nPlayers = (XP_U8)stream_getBits( stream, NPLAYERS_NBITS );
gi->boardSize = (XP_U8)stream_getBits( stream, nColsNBits );
gi->serverRole = (DeviceRole)stream_getBits( stream, 2 );
XP_LOGF( "%s: read role of %d", __func__, gi->serverRole );
gi->hintsNotAllowed = stream_getBits( stream, 1 );
if ( strVersion < STREAM_VERS_ROBOTIQ ) {
(void)stream_getBits( stream, 2 );

View file

@ -147,7 +147,7 @@ stack_getHash( const StackCtxt* stack )
stream_copyBits( stack->data, 0, stack->top, buf, &len );
// LOG_HEX( buf, len, __func__ );
hash = finishHash( augmentHash( 0L, buf, len ) );
LOG_RETURNF( "%.8X", (unsigned int)hash );
// LOG_RETURNF( "%.8X", (unsigned int)hash );
return hash;
} /* stack_getHash */
#endif

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

@ -154,7 +154,8 @@ typedef struct UtilVtable {
XP_U32 (*m_util_getCurSeconds)( XW_UtilCtxt* uc );
#ifdef XWFEATURE_DEVID
const XP_UCHAR* (*m_util_getDevID)( XW_UtilCtxt* uc, DevIDType* typ );
void (*m_util_deviceRegistered)( XW_UtilCtxt* uc, const XP_UCHAR* idRelay );
void (*m_util_deviceRegistered)( XW_UtilCtxt* uc, DevIDType typ,
const XP_UCHAR* idRelay );
#endif
DictionaryCtxt* (*m_util_makeEmptyDict)( XW_UtilCtxt* uc );
@ -284,10 +285,10 @@ struct XW_UtilCtxt {
(uc)->vtable->m_util_getCurSeconds((uc))
#ifdef XWFEATURE_DEVID
# define util_getDevID( uc, t ) \
# define util_getDevID( uc, t ) \
(uc)->vtable->m_util_getDevID((uc),(t))
# define util_deviceRegistered( uc, id ) \
(uc)->vtable->m_util_deviceRegistered( (uc), (id) )
# define util_deviceRegistered( uc, typ, id ) \
(uc)->vtable->m_util_deviceRegistered( (uc), (typ), (id) )
#endif
#define util_makeEmptyDict( uc ) \

View file

@ -111,6 +111,7 @@ DEFINES += -DXWFEATURE_HILITECELL
# allow change dict inside running game
DEFINES += -DXWFEATURE_CHANGEDICT
DEFINES += -DXWFEATURE_DEVID
DEFINES += -DXWFEATURE_COMMSACK
# MAX_ROWS controls STREAM_VERS_BIGBOARD and with it move hashing
DEFINES += -DMAX_ROWS=32

View file

@ -474,6 +474,7 @@ onetime_idle( gpointer data )
if ( !!globals->cGlobals.game.board ) {
board_draw( globals->cGlobals.game.board );
}
saveGame( &globals->cGlobals );
}
return FALSE;
}
@ -579,7 +580,7 @@ static XP_Bool
handleResend( CursesAppGlobals* globals )
{
if ( !!globals->cGlobals.game.comms ) {
comms_resendAll( globals->cGlobals.game.comms );
comms_resendAll( globals->cGlobals.game.comms, XP_TRUE );
}
return XP_TRUE;
}
@ -1219,7 +1220,7 @@ static XP_Bool
blocking_gotEvent( CursesAppGlobals* globals, int* ch )
{
XP_Bool result = XP_FALSE;
int numEvents;
int numEvents, ii;
short fdIndex;
XP_Bool redraw = XP_FALSE;
@ -1334,12 +1335,15 @@ blocking_gotEvent( CursesAppGlobals* globals, int* ch )
}
}
redraw = server_do( globals->cGlobals.game.server, NULL ) || redraw;
for ( ii = 0; ii < 5; ++ii ) {
redraw = server_do( globals->cGlobals.game.server, NULL ) || redraw;
}
if ( redraw ) {
/* messages change a lot */
board_invalAll( globals->cGlobals.game.board );
board_draw( globals->cGlobals.game.board );
}
saveGame( globals->cGlobals );
}
return result;
} /* blocking_gotEvent */
@ -1486,25 +1490,15 @@ curses_util_remSelected( XW_UtilCtxt* uc )
}
#ifndef XWFEATURE_STANDALONE_ONLY
static void
cursesSendOnClose( XWStreamCtxt* stream, void* closure )
{
CursesAppGlobals* globals = (CursesAppGlobals*)closure;
XP_LOGF( "cursesSendOnClose called" );
(void)comms_send( globals->cGlobals.game.comms, stream );
} /* cursesSendOnClose */
static XWStreamCtxt*
curses_util_makeStreamFromAddr(XW_UtilCtxt* uc, XP_PlayerAddr channelNo )
{
CursesAppGlobals* globals = (CursesAppGlobals*)uc->closure;
LaunchParams* params = globals->cGlobals.params;
XWStreamCtxt* stream = mem_stream_make( MPPARM(uc->mpool)
params->vtMgr,
uc->closure, channelNo,
cursesSendOnClose );
XWStreamCtxt* stream = mem_stream_make( MPPARM(uc->mpool) params->vtMgr,
&globals->cGlobals, channelNo,
sendOnClose );
return stream;
} /* curses_util_makeStreamFromAddr */
#endif
@ -1544,17 +1538,6 @@ setupCursesUtilCallbacks( CursesAppGlobals* globals, XW_UtilCtxt* util )
util->closure = globals;
} /* setupCursesUtilCallbacks */
#ifndef XWFEATURE_STANDALONE_ONLY
static void
sendOnClose( XWStreamCtxt* stream, void* closure )
{
CursesAppGlobals* globals = closure;
XP_LOGF( "curses sendOnClose called" );
XP_ASSERT( !!globals->cGlobals.game.comms );
comms_send( globals->cGlobals.game.comms, stream );
} /* sendOnClose */
#endif
static CursesMenuHandler
getHandlerForKey( const MenuList* list, char ch )
{
@ -1871,7 +1854,7 @@ cursesmain( XP_Bool isServer, LaunchParams* params )
server_initClientConnection( g_globals.cGlobals.game.server,
mem_stream_make( MEMPOOL
params->vtMgr,
&g_globals,
&g_globals.cGlobals,
(XP_PlayerAddr)0,
sendOnClose ) );
} else {

View file

@ -65,9 +65,6 @@
#include "filestream.h"
/* static guint gtkSetupClientSocket( GtkAppGlobals* globals, int sock ); */
#ifndef XWFEATURE_STANDALONE_ONLY
static void sendOnCloseGTK( XWStreamCtxt* stream, void* closure );
#endif
static void setCtrlsForTray( GtkAppGlobals* globals );
static void new_game( GtkWidget* widget, GtkAppGlobals* globals );
static void new_game_impl( GtkAppGlobals* globals, XP_Bool fireConnDlg );
@ -508,8 +505,8 @@ createOrLoadObjects( GtkAppGlobals* globals )
#ifndef XWFEATURE_STANDALONE_ONLY
} else if ( !isServer ) {
XWStreamCtxt* stream =
mem_stream_make( MEMPOOL params->vtMgr, globals, CHANNEL_NONE,
sendOnCloseGTK );
mem_stream_make( MEMPOOL params->vtMgr, &globals->cGlobals, CHANNEL_NONE,
sendOnClose );
server_initClientConnection( globals->cGlobals.game.server,
stream );
#endif
@ -814,11 +811,9 @@ new_game_impl( GtkAppGlobals* globals, XP_Bool fireConnDlg )
if ( isClient ) {
XWStreamCtxt* stream =
mem_stream_make( MEMPOOL
globals->cGlobals.params->vtMgr,
globals,
CHANNEL_NONE,
sendOnCloseGTK );
mem_stream_make( MEMPOOL globals->cGlobals.params->vtMgr,
&globals->cGlobals, CHANNEL_NONE,
sendOnClose );
server_initClientConnection( globals->cGlobals.game.server,
stream );
}
@ -926,7 +921,7 @@ handle_resend( GtkWidget* XP_UNUSED(widget), GtkAppGlobals* globals )
{
CommsCtxt* comms = globals->cGlobals.game.comms;
if ( comms != NULL ) {
comms_resendAll( comms );
comms_resendAll( comms, XP_TRUE );
}
} /* handle_resend */
@ -1747,8 +1742,8 @@ gtk_util_makeStreamFromAddr(XW_UtilCtxt* uc, XP_PlayerAddr channelNo )
XWStreamCtxt* stream = mem_stream_make( MEMPOOL
globals->cGlobals.params->vtMgr,
uc->closure, channelNo,
sendOnCloseGTK );
&globals->cGlobals, channelNo,
sendOnClose );
return stream;
} /* gtk_util_makeStreamFromAddr */
@ -2204,7 +2199,7 @@ gtk_socket_changed( void* closure, int oldSock, int newSock, void** storage )
/* A hack for the bluetooth case. */
CommsCtxt* comms = globals->cGlobals.game.comms;
if ( (comms != NULL) && (comms_getConType(comms) == COMMS_CONN_BT) ) {
comms_resendAll( comms );
comms_resendAll( comms, XP_FALSE );
}
LOG_RETURN_VOID();
} /* gtk_socket_changed */
@ -2270,15 +2265,6 @@ gtk_socket_acceptor( int listener, Acceptor func, CommonGlobals* globals,
}
} /* gtk_socket_acceptor */
static void
sendOnCloseGTK( XWStreamCtxt* stream, void* closure )
{
GtkAppGlobals* globals = closure;
XP_LOGF( "sendOnClose called" );
(void)comms_send( globals->cGlobals.game.comms, stream );
} /* sendOnClose */
static void
drop_msg_toggle( GtkWidget* toggle, GtkAppGlobals* globals )
{

View file

@ -196,6 +196,14 @@ catOnClose( XWStreamCtxt* stream, void* XP_UNUSED(closure) )
free( buffer );
} /* catOnClose */
void
sendOnClose( XWStreamCtxt* stream, void* closure )
{
CommonGlobals* cGlobals = (CommonGlobals*)closure;
XP_LOGF( "%s called with msg of len %d", __func__, stream_getSize(stream) );
(void)comms_send( cGlobals->game.comms, stream );
}
void
catGameHistory( CommonGlobals* cGlobals )
{
@ -1593,9 +1601,6 @@ main( int argc, char** argv )
mainParams.allowPeek = XP_TRUE;
mainParams.showRobotScores = XP_FALSE;
mainParams.useMmap = XP_TRUE;
#ifdef XWFEATURE_DEVID
mainParams.devID = "";
#endif
char* envDictPath = getenv( "XW_DICTSPATH" );
if ( !!envDictPath ) {

View file

@ -60,6 +60,8 @@ XP_UCHAR* strFromStream( XWStreamCtxt* stream );
void catGameHistory( CommonGlobals* cGlobals );
void catOnClose( XWStreamCtxt* stream, void* closure );
void sendOnClose( XWStreamCtxt* stream, void* closure );
void catFinalScores( const CommonGlobals* cGlobals, XP_S16 quitter );
XP_Bool file_exists( const char* fileName );
XWStreamCtxt* streamFromFile( CommonGlobals* cGlobals, char* name,

View file

@ -353,20 +353,37 @@ linux_util_getDevID( XW_UtilCtxt* uc, DevIDType* typ )
if ( !!cGlobals->params->rDevID ) {
*typ = ID_TYPE_RELAY;
result = cGlobals->params->rDevID;
} else {
} else if ( !!cGlobals->params->devID ) {
*typ = ID_TYPE_LINUX;
result = cGlobals->params->devID;
} else {
*typ = ID_TYPE_NONE;
result = NULL;
}
return result;
}
static void
linux_util_deviceRegistered( XW_UtilCtxt* XP_UNUSED(uc),
linux_util_deviceRegistered( XW_UtilCtxt* uc, DevIDType typ,
const XP_UCHAR* idRelay )
{
/* Script discon_ok2.sh is grepping for this in logs, so don't change
it! */
XP_LOGF( "%s: new id: %s", __func__, idRelay );
/* Script discon_ok2.sh is grepping for these strings in logs, so don't
change them! */
CommonGlobals* cGlobals = (CommonGlobals*)uc->closure;
switch( typ ) {
case ID_TYPE_NONE: /* error case */
XP_LOGF( "%s: id rejected", __func__ );
cGlobals->params->rDevID = cGlobals->params->devID = NULL;
break;
case ID_TYPE_RELAY:
if ( 0 < strlen( idRelay ) ) {
XP_LOGF( "%s: new id: %s", __func__, idRelay );
}
break;
default:
XP_ASSERT(0);
break;
}
}
#endif

View file

@ -29,6 +29,7 @@ declare -A PIDS
declare -A APPS
declare -A NEW_ARGS
declare -A ARGS
declare -A ARGS_DEVID
declare -A ROOMS
declare -A FILES
declare -A LOGS
@ -190,13 +191,14 @@ build_cmds() {
PARAMS="$PARAMS --drop-nth-packet $DROP_N $PLAT_PARMS"
# PARAMS="$PARAMS --savefail-pct 10"
[ -n "$SEED" ] && PARAMS="$PARAMS --seed $RANDOM"
# PARAMS="$PARAMS --devid LINUX_TEST_$(printf %.5d ${COUNTER})"
PARAMS="$PARAMS $PUBLIC"
ARGS[$COUNTER]=$PARAMS
ROOMS[$COUNTER]=$ROOM
FILES[$COUNTER]=$FILE
LOGS[$COUNTER]=$LOG
PIDS[$COUNTER]=0
ARGS_DEVID[$COUNTER]=""
update_devid_cmd $COUNTER
print_cmdline $COUNTER
@ -239,7 +241,7 @@ read_resume_cmds() {
launch() {
LOG=${LOGS[$1]}
APP="${APPS[$1]}"
PARAMS="${NEW_ARGS[$1]} ${ARGS[$1]}"
PARAMS="${NEW_ARGS[$1]} ${ARGS[$1]} ${ARGS_DEVID[$1]}"
exec $APP $PARAMS >/dev/null 2>>$LOG
}
@ -277,6 +279,7 @@ close_device() {
unset LOGS[$ID]
unset ROOMS[$ID]
unset APPS[$ID]
unset ARGS_DEVID[$ID]
}
OBITS=""
@ -381,16 +384,42 @@ increment_drop() {
fi
}
set_relay_devid() {
get_relayid() {
KEY=$1
CMD=${ARGS[$KEY]}
if [ "$CMD" != "${CMD/--devid //}" ]; then
RELAY_ID=$(grep 'deviceRegistered: new id: ' ${LOGS[$KEY]} | tail -n 1)
if [ -n "$RELAY_ID" ]; then
RELAY_ID=$(echo $RELAY_ID | sed 's,^.*new id: ,,')
# turn --devid <whatever> into --rdevid $RELAY_ID
ARGS[$KEY]=$(echo $CMD | sed 's,^\(.*\)--devid[ ]\+[^ ]\+\(.*\)$,\1--rdevid $RELAY_ID\2,')
RELAY_ID=$(grep 'deviceRegistered: new id: ' ${LOGS[$KEY]} | tail -n 1)
if [ -n "$RELAY_ID" ]; then
RELAY_ID=$(echo $RELAY_ID | sed 's,^.*new id: ,,')
else
usage "new id string not in $LOG"
fi
echo $RELAY_ID
}
update_devid_cmd() {
KEY=$1
HELP="$(${APPS[$KEY]} --help 2>&1 || true)"
if echo $HELP | grep -q '\-\-devid'; then
CMD="--devid LINUX_TEST_$(printf %.5d ${KEY})"
LOG=${LOGS[$KEY]}
if [ -z "${ARGS_DEVID[$KEY]}" -o ! -e $LOG ]; then # upgrade or first run
:
else
# otherwise, we should have successfully registered. If
# we have AND the reg has been rejected, make without
# --rdevid so will reregister.
LAST_GOOD=$(grep -h -n 'linux_util_deviceRegistered: new id' $LOG | tail -n 1 | sed 's,:.*$,,')
LAST_REJ=$(grep -h -n 'linux_util_deviceRegistered: id rejected' $LOG | tail -n 1 | sed 's,:.*$,,')
# echo "LAST_GOOD: $LAST_GOOD; LAST_REJ: $LAST_REJ"
if [ -z "$LAST_GOOD" ]; then # not yet registered
:
elif [ -z "$LAST_REJ" ]; then
CMD="$CMD --rdevid $(get_relayid $KEY)"
elif [ "$LAST_REJ" -lt "$LAST_GOOD" ]; then # registered and not more recently rejected
CMD="$CMD --rdevid $(get_relayid $KEY)"
fi
fi
# echo $CMD
ARGS_DEVID[$KEY]=$CMD
fi
}
@ -423,7 +452,7 @@ run_cmds() {
PIDS[$KEY]=0
ROOM_PIDS[$ROOM]=0
[ "$DROP_N" -ge 0 ] && increment_drop $KEY
# set_relay_devid $KEY
update_devid_cmd $KEY
check_game $KEY
fi
done

View file

@ -45,6 +45,7 @@ CPPFLAGS += -DSPAWN_SELF -g -Wall \
-I $(shell pg_config --includedir) \
-DSVN_REV=\"$(shell cat $(GITINFO) 2>/dev/null || echo -n $(HASH) )\"
# CPPFLAGS += -DDO_HTTP
# CPPFLAGS += -DHAVE_STIME
# turn on semaphore debugging
# CPPFLAGS += -DDEBUG_LOCKS

View file

@ -627,6 +627,7 @@ CookieRef::handleEvents()
/* Assumption: has mutex!!!! */
while ( m_eventQueue.size () > 0 ) {
XW_RELAY_STATE nextState;
DBMgr::DevIDRelay devID;
CRefEvent evt = m_eventQueue.front();
m_eventQueue.pop_front();
@ -642,7 +643,6 @@ CookieRef::handleEvents()
case XWA_SEND_CONNRSP:
{
HostID hid;
DBMgr::DevIDRelay devID;
if ( increasePlayerCounts( &evt, false, &hid, &devID ) ) {
setAllConnectedTimer();
sendResponse( &evt, true, &devID );
@ -668,8 +668,8 @@ CookieRef::handleEvents()
/* break; */
case XWA_SEND_RERSP:
increasePlayerCounts( &evt, true, NULL, NULL );
sendResponse( &evt, false, NULL );
increasePlayerCounts( &evt, true, NULL, &devID );
sendResponse( &evt, false, &devID );
sendAnyStored( &evt );
postCheckAllHere();
break;
@ -891,13 +891,7 @@ CookieRef::increasePlayerCounts( CRefEvent* evt, bool reconn, HostID* hidp,
DevIDType devIDType = evt->u.con.devID->m_devIDType;
// does client support devID
if ( ID_TYPE_NONE != devIDType ) {
// have we not already converted it?
if ( ID_TYPE_RELAY == devIDType ) {
devID = (DBMgr::DevIDRelay)strtoul( evt->u.con.devID->m_devIDString.c_str(),
NULL, 16 );
} else {
devID = DBMgr::Get()->RegisterDevice( evt->u.con.devID );
}
devID = DBMgr::Get()->RegisterDevice( evt->u.con.devID );
}
*devIDp = devID;
}
@ -1070,20 +1064,34 @@ CookieRef::sendResponse( const CRefEvent* evt, bool initial,
memcpy( bufp, connName, len );
bufp += len;
if ( initial ) {
// we always write at least empty string
char idbuf[MAX_DEVID_LEN + 1] = {0};
// we always write at least empty string
// If client supports devid, and we have one (response case), write it as
// 8-byte hex string plus a length byte -- but only if we didn't already
// receive it.
if ( !!devID && ID_TYPE_RELAY < evt->u.con.devID->m_devIDType ) {
// If client supports devid, and we have one (response case), write it as
// 8-byte hex string plus a length byte -- but only if we didn't already
// receive it.
// there are three possibilities: it sent us a platform-specific ID and we
// need to return the relay version; or it sent us a valid relay version;
// or it sent us an invalid one (for whatever reason, e.g. we've wiped the
// devices table entry for a problematic GCM id to force reregistration.)
// In the first case, we return the new relay version. In the second, we
// return that the type is ID_TYPE_RELAY but don't bother with the version
// string; and in the third, we return ID_TYPE_NONE.
if ( DBMgr::DEVID_NONE == *devID ) { // first case
*bufp++ = ID_TYPE_NONE;
} else {
*bufp++ = ID_TYPE_RELAY;
// Write an empty string if the client passed the ID to us, or the id
// if it's new to the client.
char idbuf[MAX_DEVID_LEN + 1];
if ( !!ID_TYPE_RELAY < evt->u.con.devID->m_devIDType ) {
len = snprintf( idbuf, sizeof(idbuf), "%.8X", *devID );
assert( len < sizeof(idbuf) );
} else {
len = 0;
}
len = strlen( idbuf );
assert( len <= MAX_DEVID_LEN );
*bufp++ = (char)len;
if ( 0 < len ) {
memcpy( bufp, idbuf, len );

View file

@ -616,13 +616,13 @@ SafeCref::SafeCref( const char* cookie, int socket, int clientVersion,
/* REconnect case */
SafeCref::SafeCref( const char* connName, const char* cookie, HostID hid,
int socket, int clientVersion, int nPlayersH, int nPlayersS,
unsigned short gameSeed, int langCode,
int socket, int clientVersion, DevID* devID, int nPlayersH,
int nPlayersS, unsigned short gameSeed, int langCode,
bool wantsPublic, bool makePublic )
: m_cinfo( NULL )
, m_mgr( CRefMgr::Get() )
, m_clientVersion( clientVersion )
, m_devID( NULL )
, m_devID( devID )
, m_isValid( false )
{
CidInfo* cinfo;

View file

@ -177,8 +177,8 @@ class SafeCref {
bool makePublic );
/* for reconnect */
SafeCref( const char* connName, const char* cookie, HostID hid,
int socket, int clientVersion, int nPlayersH, int nPlayersS,
unsigned short gameSeed, int langCode,
int socket, int clientVersion, DevID* devID, int nPlayersH,
int nPlayersS, unsigned short gameSeed, int langCode,
bool wantsPublic, bool makePublic );
SafeCref( const char* const connName );
SafeCref( CookieID cid, bool failOk = false );

View file

@ -47,6 +47,7 @@ static void formatParams( char* paramValues[], int nParams, const char* fmt,
static int here_less_seed( const char* seeds, int perDeviceSum,
unsigned short seed );
static void destr_function( void* conn );
static void string_printf( string& str, const char* fmt, ... );
/* static */ DBMgr*
DBMgr::Get()
@ -127,11 +128,11 @@ DBMgr::FindGame( const char* connName, char* cookieBuf, int bufLen,
const char* fmt = "SELECT cid, room, lang, nTotal, nPerDevice, dead FROM "
GAMES_TABLE " WHERE connName = '%s'"
" LIMIT 1";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "query: %s", query );
string query;
string_printf( query, fmt, connName );
logf( XW_LOGINFO, "query: %s", query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( cookieBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
@ -234,11 +235,11 @@ DBMgr::AllDevsAckd( const char* const connName )
{
const char* cmd = "SELECT ntotal=sum_array(nperdevice) AND 'A'=ALL(ack) from " GAMES_TABLE
" WHERE connName='%s'";
char query[256];
snprintf( query, sizeof(query), cmd, connName );
logf( XW_LOGINFO, "query: %s", query );
string query;
string_printf( query, cmd, connName );
logf( XW_LOGINFO, "query: %s", query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
int nTuples = PQntuples( result );
assert( nTuples <= 1 );
bool full = nTuples == 1 && 't' == PQgetvalue( result, 0, 0 )[0];
@ -253,13 +254,16 @@ DBMgr::DevIDRelay
DBMgr::RegisterDevice( const DevID* host )
{
DBMgr::DevIDRelay devID;
assert( host->m_devIDType != ID_TYPE_RELAY );
assert( host->m_devIDType != ID_TYPE_NONE );
int ii;
bool success;
// if it's already present, just return
devID = getDevID( host );
if ( DEVID_NONE == devID ) {
// If it's not present *and* of type ID_TYPE_RELAY, we can do nothing.
// Fail.
if ( DEVID_NONE == devID && ID_TYPE_RELAY < host->m_devIDType ) {
// loop until we're successful inserting the unique key. Ship with this
// coming from random, but test with increasing values initially to make
// sure duplicates are detected.
@ -314,10 +318,11 @@ DBMgr::AddDevice( const char* connName, HostID curID, int clientVersion,
}
assert( newID <= 4 );
char devIDBuf[512] = {0};
string devIDBuf;
if ( DEVID_NONE != devID ) {
snprintf( devIDBuf, sizeof(devIDBuf),
"devids[%d] = %d, ", newID, devID );
string_printf( devIDBuf, "devids[%d] = %d, ", newID, devID );
} else {
assert( 0 == strlen(devIDBuf.c_str()) );
}
const char* fmt = "UPDATE " GAMES_TABLE " SET nPerDevice[%d] = %d,"
@ -325,11 +330,11 @@ DBMgr::AddDevice( const char* connName, HostID curID, int clientVersion,
" seeds[%d] = %d, addrs[%d] = \'%s\', %s"
" mtimes[%d]='now', ack[%d]=\'%c\'"
" WHERE connName = '%s'";
char query[1024];
snprintf( query, sizeof(query), fmt, newID, nToAdd, newID, clientVersion,
newID, seed, newID, inet_ntoa(addr), devIDBuf,
newID, newID, ackd?'A':'a', connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, newID, nToAdd, newID, clientVersion,
newID, seed, newID, inet_ntoa(addr), devIDBuf.c_str(),
newID, newID, ackd?'A':'a', connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
@ -339,11 +344,11 @@ DBMgr::AddDevice( const char* connName, HostID curID, int clientVersion,
void
DBMgr::NoteAckd( const char* const connName, HostID id )
{
char query[256];
const char* fmt = "UPDATE " GAMES_TABLE " SET ack[%d]='A'"
" WHERE connName = '%s'";
snprintf( query, sizeof(query), fmt, id, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, id, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@ -353,9 +358,9 @@ DBMgr::RmDeviceByHid( const char* connName, HostID hid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET nPerDevice[%d] = 0, "
"seeds[%d] = 0, ack[%d]='-', mtimes[%d]='now' WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, hid, hid, hid, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, hid, hid, hid, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
return execSql( query );
}
@ -368,10 +373,10 @@ DBMgr::HIDForSeed( const char* const connName, unsigned short seed )
const char* fmt = "SELECT seeds FROM " GAMES_TABLE
" WHERE connName = '%s'"
" AND %d = ANY(seeds)";
char query[256];
snprintf( query, sizeof(query), fmt, connName, seed );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
string query;
string_printf( query, fmt, connName, seed );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
snprintf( seeds, sizeof(seeds), "%s", PQgetvalue( result, 0, 0 ) );
}
@ -415,10 +420,10 @@ DBMgr::HaveDevice( const char* connName, HostID hid, int seed )
bool found = false;
const char* fmt = "SELECT * from " GAMES_TABLE
" WHERE connName = '%s' AND seeds[%d] = %d";
char query[256];
snprintf( query, sizeof(query), fmt, connName, hid, seed );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
PGresult* result = PQexec( getThreadConn(), query );
string query;
string_printf( query, fmt, connName, hid, seed );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
found = 1 == PQntuples( result );
PQclear( result );
return found;
@ -429,9 +434,9 @@ DBMgr::AddCID( const char* const connName, CookieID cid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET cid = %d "
" WHERE connName = '%s' AND cid IS NULL";
char query[256];
snprintf( query, sizeof(query), fmt, cid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, cid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
bool result = execSql( query );
logf( XW_LOGINFO, "%s(cid=%d)=>%d", __func__, cid, result );
@ -443,9 +448,9 @@ DBMgr::ClearCID( const char* connName )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET cid = null "
"WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@ -457,9 +462,9 @@ DBMgr::RecordSent( const char* const connName, HostID hid, int nBytes )
const char* fmt = "UPDATE " GAMES_TABLE " SET"
" nsent = nsent + %d, mtimes[%d] = 'now'"
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, nBytes, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, nBytes, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@ -468,23 +473,19 @@ void
DBMgr::RecordSent( const int* msgIDs, int nMsgIDs )
{
if ( nMsgIDs > 0 ) {
char buf[1024];
unsigned int offset = 0;
offset = snprintf( buf, sizeof(buf), "SELECT connname,hid,sum(msglen)"
" FROM " MSGS_TABLE " WHERE id IN (" );
string query( "SELECT connname,hid,sum(msglen)"
" FROM " MSGS_TABLE " WHERE id IN (" );
for ( int ii = 0; ; ) {
offset += snprintf( &buf[offset], sizeof(buf) - offset, "%d,",
msgIDs[ii] );
assert( offset < sizeof(buf) );
string_printf( query, "%d", msgIDs[ii] );
if ( ++ii == nMsgIDs ) {
--offset; /* back over comma */
break;
} else {
query.append( "," );
}
}
offset += snprintf( &buf[offset], sizeof(buf) - offset,
") GROUP BY connname,hid" );
query.append( ") GROUP BY connname,hid" );
PGresult* result = PQexec( getThreadConn(), buf );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( PGRES_TUPLES_OK == PQresultStatus( result ) ) {
int ntuples = PQntuples( result );
for ( int ii = 0; ii < ntuples; ++ii ) {
@ -504,9 +505,9 @@ DBMgr::RecordAddress( const char* const connName, HostID hid,
assert( hid >= 0 && hid <= 4 );
const char* fmt = "UPDATE " GAMES_TABLE " SET addrs[%d] = \'%s\'"
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, hid, inet_ntoa(addr), connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, hid, inet_ntoa(addr), connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
@ -516,11 +517,11 @@ DBMgr::GetPlayerCounts( const char* const connName, int* nTotal, int* nHere )
{
const char* fmt = "SELECT ntotal, sum_array(nperdevice) FROM " GAMES_TABLE
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
*nTotal = atoi( PQgetvalue( result, 0, 0 ) );
*nHere = atoi( PQgetvalue( result, 0, 1 ) );
@ -530,11 +531,11 @@ DBMgr::GetPlayerCounts( const char* const connName, int* nTotal, int* nHere )
void
DBMgr::KillGame( const char* const connName, int hid )
{
const char* fmt = "UPDATE " GAMES_TABLE " SET dead = TRUE,"
" nperdevice[%d] = - nperdevice[%d]"
" WHERE connName = '%s'";
char query[256];
snprintf( query, sizeof(query), fmt, hid, hid, connName );
const char* fmt = "UPDATE " GAMES_TABLE " SET dead = TRUE,"
" nperdevice[%d] = - nperdevice[%d]"
" WHERE connName = '%s'";
string query;
string_printf( query, fmt, hid, hid, connName );
execSql( query );
}
@ -556,11 +557,11 @@ DBMgr::PublicRooms( int lang, int nPlayers, int* nNames, string& names )
" AND nTotal>sum_array(nPerDevice)"
" AND nTotal = %d";
char query[256];
snprintf( query, sizeof(query), fmt, lang, nPlayers );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, lang, nPlayers );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
int nTuples = PQntuples( result );
for ( int ii = 0; ii < nTuples; ++ii ) {
names.append( PQgetvalue( result, ii, 0 ) );
@ -579,12 +580,16 @@ DBMgr::PendingMsgCount( const char* connName, int hid )
{
int count = 0;
const char* fmt = "SELECT COUNT(*) FROM " MSGS_TABLE
" WHERE connName = '%s' AND hid = %d";
char query[256];
snprintf( query, sizeof(query), fmt, connName, hid );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
" WHERE connName = '%s' AND hid = %d "
#ifdef HAVE_STIME
"AND stime IS NULL"
#endif
;
string query;
string_printf( query, fmt, connName, hid );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
if ( 1 == PQntuples( result ) ) {
count = atoi( PQgetvalue( result, 0, 0 ) );
}
@ -592,6 +597,12 @@ DBMgr::PendingMsgCount( const char* connName, int hid )
return count;
}
bool
DBMgr::execSql( const string& query )
{
return execSql( query.c_str() );
}
bool
DBMgr::execSql( const char* const query )
{
@ -609,11 +620,11 @@ DBMgr::readArray( const char* const connName, int arr[] ) /* len 4 */
{
const char* fmt = "SELECT nPerDevice FROM " GAMES_TABLE " WHERE connName='%s'";
char query[256];
snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
const char* arrStr = PQgetvalue( result, 0, 0 );
sscanf( arrStr, "{%d,%d,%d,%d}", &arr[0], &arr[1], &arr[2], &arr[3] );
@ -625,11 +636,11 @@ DBMgr::getDevID( const char* connName, int hid )
{
DBMgr::DevIDRelay devID;
const char* fmt = "SELECT devids[%d] FROM " GAMES_TABLE " WHERE connName='%s'";
char query[256];
snprintf( query, sizeof(query), fmt, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, hid, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
devID = (DBMgr::DevIDRelay)strtoul( PQgetvalue( result, 0, 0 ), NULL, 10 );
PQclear( result );
@ -641,17 +652,24 @@ DBMgr::getDevID( const DevID* devID )
{
DBMgr::DevIDRelay rDevID = DEVID_NONE;
DevIDType devIDType = devID->m_devIDType;
string query;
assert( ID_TYPE_NONE < devIDType );
const char* asStr = devID->m_devIDString.c_str();
if ( ID_TYPE_RELAY == devIDType ) {
rDevID = strtoul( asStr, NULL, 16 );
// confirm it's there
DBMgr::DevIDRelay cur = strtoul( asStr, NULL, 16 );
if ( DEVID_NONE != cur ) {
const char* fmt = "SELECT id FROM " DEVICES_TABLE " WHERE id=%d";
string_printf( query, fmt, cur );
}
} else {
const char* fmt = "SELECT id FROM " DEVICES_TABLE " WHERE devtype=%d and devid = '%s'";
char query[512];
snprintf( query, sizeof(query), fmt, devIDType, asStr );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string_printf( query, fmt, devIDType, asStr );
}
PGresult* result = PQexec( getThreadConn(), query );
if ( 0 < query.size() ) {
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 >= PQntuples( result ) );
if ( 1 == PQntuples( result ) ) {
rDevID = (DBMgr::DevIDRelay)strtoul( PQgetvalue( result, 0, 0 ), NULL, 10 );
@ -673,18 +691,20 @@ int
DBMgr::CountStoredMessages( const char* const connName, int hid )
{
const char* fmt = "SELECT count(*) FROM " MSGS_TABLE
" WHERE connname = '%s' ";
" WHERE connname = '%s' "
#ifdef HAVE_STIME
"AND stime IS NULL"
#endif
;
char query[256];
int len = snprintf( query, sizeof(query), fmt, connName );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
string query;
string_printf( query, fmt, connName );
if ( hid != -1 ) {
snprintf( &query[len], sizeof(query)-len, "AND hid = %d",
hid );
string_printf( query, "AND hid = %d", hid );
}
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
assert( 1 == PQntuples( result ) );
int count = atoi( PQgetvalue( result, 0, 0 ) );
PQclear( result );
@ -712,18 +732,13 @@ DBMgr::StoreMessage( const char* const connName, int hid,
len, &newLen );
assert( NULL != bytes );
char query[1024];
size_t siz = snprintf( query, sizeof(query), fmt, connName, hid,
devID, bytes, len );
string query;
string_printf( query, fmt, connName, hid, devID, bytes, len );
PQfreemem( bytes );
if ( siz < sizeof(query) ) {
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
execSql( query );
} else {
logf( XW_LOGERROR, "%s: buffer too small", __func__ );
}
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
bool
@ -732,12 +747,16 @@ DBMgr::GetNthStoredMessage( const char* const connName, int hid,
int* msgID )
{
const char* fmt = "SELECT id, msg, msglen FROM " MSGS_TABLE
" WHERE connName = '%s' AND hid = %d ORDER BY id LIMIT 1 OFFSET %d";
char query[256];
snprintf( query, sizeof(query), fmt, connName, hid, nn );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
" WHERE connName = '%s' AND hid = %d "
#ifdef HAVE_STIME
"AND stime IS NULL "
#endif
"ORDER BY id LIMIT 1 OFFSET %d";
string query;
string_printf( query, fmt, connName, hid, nn );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
PGresult* result = PQexec( getThreadConn(), query );
PGresult* result = PQexec( getThreadConn(), query.c_str() );
int nTuples = PQntuples( result );
assert( nTuples <= 1 );
@ -774,22 +793,29 @@ void
DBMgr::RemoveStoredMessages( const int* msgIDs, int nMsgIDs )
{
if ( nMsgIDs > 0 ) {
char ids[1024];
string ids;
size_t len = 0;
int ii;
for ( ii = 0; ; ) {
len += snprintf( ids + len, sizeof(ids) - len, "%d,", msgIDs[ii] );
string_printf( ids, "%d", msgIDs[ii] );
assert( len < sizeof(ids) );
if ( ++ii == nMsgIDs ) {
ids[len-1] = '\0'; /* overwrite last comma */
break;
} else {
ids.append( "," );
}
}
const char* fmt = "DELETE from " MSGS_TABLE " WHERE id in (%s)";
char query[1024];
snprintf( query, sizeof(query), fmt, ids );
logf( XW_LOGINFO, "%s: query: %s", __func__, query );
const char* fmt =
#ifdef HAVE_STIME
"UPDATE " MSGS_TABLE " SET stime='now' "
#else
"DELETE FROM " MSGS_TABLE
#endif
" WHERE id IN (%s)";
string query;
string_printf( query, fmt, ids.c_str() );
logf( XW_LOGINFO, "%s: query: %s", __func__, query.c_str() );
execSql( query );
}
}
@ -852,3 +878,29 @@ DBMgr::getThreadConn( void )
}
return conn;
}
/* From stack overflow, toward a snprintf with an expanding buffer.
*/
static void
string_printf( string& str, const char* fmt, ... )
{
const int origsiz = str.size();
int newsiz = 100;
va_list ap;
for ( ; ; ) {
str.resize( origsiz + newsiz );
va_start( ap, fmt );
int len = vsnprintf( (char *)str.c_str() + origsiz, newsiz, fmt, ap );
va_end( ap );
if ( len > newsiz ) { // needs more space
newsiz = len + 1;
} else if ( -1 == len ) {
assert(0); // should be impossible
} else {
str.resize( origsiz + len );
break;
}
}
}

View file

@ -105,6 +105,7 @@ class DBMgr {
private:
DBMgr();
bool execSql( const string& query );
bool execSql( const char* const query ); /* no-results query */
void readArray( const char* const connName, int arr[] );
DevIDRelay getDevID( const char* connName, int hid );

View file

@ -6,7 +6,7 @@
#
# Depends on the gcm module
import getpass, sys, gcm, psycopg2, time, signal
import getpass, sys, gcm, psycopg2, time, signal, shelve
from time import gmtime, strftime
# I'm not checking my key in...
@ -25,108 +25,147 @@ import mykey
# contact list if it is the target of at least one message in the msgs
# table.
k_shelfFile = "gcm_loop.shelf"
k_SENT = 'SENT'
g_con = None
g_sent = None
g_debug = False
g_skipSend = False # for debugging
DEVTYPE = 3 # 3 == GCM
DEVTYPE_GCM = 3 # 3 == GCM
LINE_LEN = 76
def init():
global g_sent
try:
con = psycopg2.connect(database='xwgames', user=getpass.getuser())
except psycopg2.DatabaseError, e:
print 'Error %s' % e
sys.exit(1)
shelf = shelve.open( k_shelfFile )
if k_SENT in shelf: g_sent = shelf[k_SENT]
else: g_sent = {}
shelf.close();
if g_debug: print 'g_sent:', g_sent
return con
def getPendingMsgs( con ):
# WHERE stime IS NULL
def getPendingMsgs( con, typ ):
cur = con.cursor()
cur.execute("SELECT id, devid FROM msgs WHERE devid IN (SELECT id FROM devices WHERE devtype=%d)" % DEVTYPE)
query = """SELECT id, devid FROM msgs
WHERE devid IN (SELECT id FROM devices WHERE devtype=%d and NOT unreg)
AND NOT connname IN (SELECT connname FROM games WHERE dead); """
cur.execute(query % typ)
result = cur.fetchall()
if g_debug: print "getPendingMsgs=>", result
return result
def asGCMIds(con, devids):
def unregister( gcmid ):
global g_con
print "unregister(", gcmid, ")"
query = "UPDATE devices SET unreg=TRUE WHERE id = '%s'" % gcmid
g_con.cursor().execute( query )
def asGCMIds(con, devids, typ):
cur = con.cursor()
query = "SELECT devid FROM devices WHERE devtype = %d AND id IN (%s)" \
% (DEVTYPE, ",".join([str(y) for y in devids]))
% (typ, ",".join([str(y) for y in devids]))
cur.execute( query )
return [elem[0] for elem in cur.fetchall()]
def notifyGCM( devids ):
instance = gcm.GCM( mykey.myKey )
data = { 'getMoves': True,
# 'title' : 'Msg from Darth',
# 'msg' : "I am your father, Luke.",
}
# JSON request
response = instance.json_request( registration_ids = devids,
data = data )
if 'errors' in response:
for error, reg_ids in response.items():
print error
def notifyGCM( devids, typ ):
if typ == DEVTYPE_GCM:
instance = gcm.GCM( mykey.myKey )
data = { 'getMoves': True, }
response = instance.json_request( registration_ids = devids,
data = data,
)
if 'errors' in response:
response = response['errors']
if 'NotRegistered' in response:
for gcmid in response['NotRegistered']:
unregister( gcmid )
else:
print "got some kind of error"
else:
if g_debug: print 'no errors:', response
else:
print 'no errors'
print "not sending to", len(devids), "devices because typ ==", typ
def shouldSend(val):
pow = 1
while pow < val:
pow *= 2
return pow == val
return val == 1
# pow = 1
# while pow < val:
# pow *= 3
# return pow == val
# given a list of msgid, devid lists, figure out which messages should
# be sent/resent now and mark them as sent. Backoff is based on
# msgids: if the only messages a device has pending have been seen
# before, backoff applies.
def targetsAfterBackoff( msgs, sent ):
targets = []
def targetsAfterBackoff( msgs ):
global g_sent
targets = {}
for row in msgs:
msgid = row[0]
if not msgid in sent:
sent[msgid] = 0
sent[msgid] += 1
if shouldSend( sent[msgid] ):
targets.append( row[1] )
return targets
devid = row[1]
if not msgid in g_sent:
g_sent[msgid] = 0
g_sent[msgid] += 1
if shouldSend( g_sent[msgid] ):
targets[devid] = True
return targets.keys()
# devids is an array of (msgid, devid) tuples
def pruneSent( devids, sent ):
if g_debug: print "pruneSent: before:", sent
lenBefore = len(sent)
def pruneSent( devids ):
global g_sent
if g_debug: print "pruneSent: before:", g_sent
lenBefore = len(g_sent)
msgids = []
for row in devids:
msgids.append(row[0])
for msgid in sent.keys():
for msgid in g_sent.keys():
if not msgid in msgids:
del sent[msgid]
if g_debug: print "pruneSent: after:", sent
return sent
del g_sent[msgid]
if g_debug: print "pruneSent: after:", g_sent
def cleanup():
global g_con, g_sent
if g_con:
g_con.close()
g_con = None
shelf = shelve.open( k_shelfFile )
shelf[k_SENT] = g_sent
shelf.close();
def handleSigTERM( one, two ):
print 'handleSigTERM called: ', one, two
global g_con
if g_con:
g_con.close()
g_con = None
cleanup()
def usage():
print "usage:", sys.argv[0], "[--loop]"
print "usage:", sys.argv[0], "[--loop <nSeconds>] [--type typ] [--verbose]"
sys.exit();
def main():
global g_con
global g_con, g_sent, g_debug
loopInterval = 0
g_con = init()
emptyCount = 0
typ = DEVTYPE_GCM
ii = 1
while ii < len(sys.argv):
arg = sys.argv[ii]
if arg == '--loop':
ii = ii + 1
ii += 1
loopInterval = float(sys.argv[ii])
elif arg == '--type':
ii += 1
typ = int(sys.argv[ii])
elif arg == '--verbose':
g_debug = True
else:
usage()
ii = ii + 1
@ -134,30 +173,29 @@ def main():
signal.signal( signal.SIGTERM, handleSigTERM )
signal.signal( signal.SIGINT, handleSigTERM )
sent = {}
while g_con:
devids = getPendingMsgs( g_con )
if g_debug: print
devids = getPendingMsgs( g_con, typ )
if 0 < len(devids):
targets = targetsAfterBackoff( devids, sent )
targets = targetsAfterBackoff( devids )
if 0 < len(targets):
if 0 < emptyCount: print ""
emptyCount = 0
print strftime("%Y-%m-%d %H:%M:%S", gmtime()),
print strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
print "devices needing notification:", targets
if not g_skipSend:
notifyGCM( asGCMIds( g_con, targets ) )
pruneSent( devids, sent )
else:
notifyGCM( asGCMIds( g_con, targets, typ ), typ )
pruneSent( devids )
elif g_debug: print "no targets after backoff"
else:
emptyCount += 1
if (0 == (emptyCount%5)) and not g_debug:
sys.stdout.write('.')
sys.stdout.flush()
emptyCount = emptyCount + 1
if 0 == (emptyCount % LINE_LEN): print ""
if 0 == (emptyCount % (LINE_LEN*5)): print ""
if 0 == loopInterval: break
time.sleep( loopInterval )
if g_debug: print
if g_con:
g_con.close()
cleanup()
##############################################################################
if __name__ == '__main__':

View file

@ -1,10 +1,13 @@
#!/usr/bin/python
import sys, gcm, psycopg2
import sys, gcm, psycopg2, json
# I'm not checking my key in...
import mykey
def usage():
print 'usage:', sys.argv[0], '[--to <name>] msg'
sys.exit()
def msgViaGCM( devid, msg ):
instance = gcm.GCM( mykey.myKey )
@ -14,18 +17,30 @@ def msgViaGCM( devid, msg ):
response = instance.json_request( registration_ids = [devid],
data = data )
if 'errors' in response:
for error, reg_ids in response.items():
print error
response = response['errors']
if 'NotRegistered' in response:
ids = response['NotRegistered']
for id in ids:
print 'need to remove "', id, '" from db'
else:
print 'no errors'
def main():
to = None
msg = sys.argv[1]
print 'got "%s"' % msg
msgViaGCM( mykey.myBlaze, msg )
if msg == '--to':
to = sys.argv[2]
msg = sys.argv[3]
elif 2 < len(sys.argv):
usage()
if not to in mykey.devids.keys():
print 'Unknown --to param;', to, 'not in', ','.join(mykey.devids.keys())
usage()
if not to: usage()
devid = mykey.devids[to]
print 'sending: "%s" to' % msg, to
msgViaGCM( devid, msg )
##############################################################################
if __name__ == '__main__':

View file

@ -98,6 +98,7 @@ $cols = array( new Column("dead", "D", "capitalize", false ),
new Column("clntVers", "CV", "identity", true ),
new Column("nperdevice", "NP", "identity", true ),
new Column("ack", "A", "identity", true ),
new Column("devids", "DevIDs", "identity", true ),
new Column("nsent", "Sent", "identity", false ),
new Column("addrs", "Dev. addr", "ip_to_host", true ),
new Column("ctime", "Created", "print_date", false ),

View file

@ -242,6 +242,21 @@ getNetString( unsigned char** bufpp, const unsigned char* end, string& out )
return success;
}
static void
getDevID( unsigned char** bufpp, const unsigned char* end,
unsigned short flags, DevID* devID )
{
if ( XWRELAY_PROTO_VERSION_CLIENTID <= flags ) {
unsigned char devIDType = 0;
if ( getNetByte( bufpp, end, &devIDType ) && 0 != devIDType ) {
if ( getNetString( bufpp, end, devID->m_devIDString )
&& 0 < devID->m_devIDString.length() ) {
devID->m_devIDType = (DevIDType)devIDType;
}
}
}
}
#ifdef RELAY_HEARTBEAT
static bool
processHeartbeat( unsigned char* buf, int bufLen, int socket )
@ -381,16 +396,7 @@ processConnect( unsigned char* bufp, int bufLen, int socket, in_addr& addr )
&& getNetByte( &bufp, end, &langCode ) ) {
DevID devID;
if ( XWRELAY_PROTO_VERSION_CLIENTID <= flags ) {
unsigned char devIDType = 0;
if ( getNetByte( &bufp, end, &devIDType )
&& 0 != devIDType ) {
if ( getNetString( &bufp, end, devID.m_devIDString )
&& 0 < devID.m_devIDString.length() ) {
devID.m_devIDType = (DevIDType)devIDType;
}
}
}
getDevID( &bufp, end, flags, &devID );
logf( XW_LOGINFO, "%s(): langCode=%d; nPlayersT=%d; "
"wantsPublic=%d; seed=%.4X",
@ -449,9 +455,12 @@ processReconnect( unsigned char* bufp, int bufLen, int socket, in_addr& addr )
&& getNetByte( &bufp, end, &langCode )
&& readStr( &bufp, end, connName, sizeof(connName) ) ) {
DevID devID;
getDevID( &bufp, end, flags, &devID );
SafeCref scr( connName[0]? connName : NULL,
cookie, srcID, socket, clientVersion, nPlayersH,
nPlayersT, gameSeed, langCode,
cookie, srcID, socket, clientVersion, &devID,
nPlayersH, nPlayersT, gameSeed, langCode,
wantsPublic, makePublic );
success = scr.Reconnect( socket, srcID, nPlayersH, nPlayersT,
gameSeed, addr, &err );

View file

@ -68,6 +68,7 @@ id SERIAL
,connName VARCHAR(64)
,hid INTEGER
,ctime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
,stime TIMESTAMP DEFAULT NULL
,devid INTEGER
,msg BYTEA
,msglen INTEGER
@ -81,6 +82,7 @@ id INTEGER UNIQUE PRIMARY KEY
,devType INTEGER
,devid TEXT
,ctime TIMESTAMP DEFAULT CURRENT_TIMESTAMP
,unreg BOOLEAN DEFAULT FALSE
);
EOF
}