mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2025-01-01 06:19:57 +01:00
Merge branch 'android_branch' into fix_dlgdelegate
This commit is contained in:
commit
110df3c1ba
86 changed files with 3293 additions and 2145 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,3 +5,4 @@ TAGS
|
|||
core
|
||||
*.apk
|
||||
xwords_4.4.0.0*
|
||||
gcm_loop.shelf
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
/>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
// }
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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 ) {}
|
||||
}
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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 );
|
||||
|
|
112
xwords4/android/scripts/and_index.php
Normal file
112
xwords4/android/scripts/and_index.php
Normal 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'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'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'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();
|
||||
|
||||
|
||||
?>
|
|
@ -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
|
||||
|
|
|
@ -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's probably because you don'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'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 "Add game", either from the menu or the button at the bottom.</li>
|
||||
<li>Under "New Networked game", choose "Configure first".</li>
|
||||
<li>$langText</li>
|
||||
<li>As the room name, enter "$room".</li>
|
||||
<li>Make sure the total number of players shown is $np and that only one of them is not an "Off-device player".</li>
|
||||
<li>Now tap the "Play game" button at the bottom (above the keyboard). Your new game should open and connect.</li>
|
||||
</ol></p>
|
||||
<p>I'm sorry this is so complicated. I'm trying to find a
|
||||
workaround for this limitation in the Kindle Fire's operating system
|
||||
but for now this is all I can offer.</p>
|
||||
|
||||
<p>(Just in case Amazon'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;
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 ) \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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 ),
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue