Merge branch 'android_branch' into android_groups

Conflicts:
	xwords4/android/XWords4/res/values/strings.xml
	xwords4/android/XWords4/src/org/eehouse/android/xw4/DBHelper.java
	xwords4/android/XWords4/src/org/eehouse/android/xw4/GamesList.java
	xwords4/android/XWords4/src/org/eehouse/android/xw4/XWApp.java
This commit is contained in:
Eric House 2012-12-01 10:52:42 -08:00
commit c4e638bd84
35 changed files with 671 additions and 639 deletions

View file

@ -129,8 +129,19 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http"
android:host="eehouse.org" android:pathPrefix="/and" />
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="*/*" -->
<!-- android:scheme="content" -->
<!-- /> -->
<!-- </intent-filter> -->
</activity>
<!-- downloading dicts -->

View file

@ -7,13 +7,18 @@
<body>
<b>Crosswords 4.4 beta 56 release</b>
<ul>New with this release
<li>Improve invitations: no more redirection through a website, and
confirm before creating duplicate games</li>
<li>(For sideloading users only) No more browser involvement in
updates: app can launch the installer directly to update
itself</li>
<li>Remove notifications when their games are deleted</li>
</ul>
<ul>Next up
<li>Make invitations more reliable</li>
<li>Upgrade app without involving the browser</li>
<li>One more idea for improving invitations</li>
<li>Allow grouping of games in collapsible user-defined categores: "Games with
Kati", "Finished games", etc.</li>
</ul>
<p>(The full changelog

View file

@ -30,7 +30,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>
@ -103,9 +102,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/and</string>
<string name="expl_update_url">Update checks URL</string>
<string name="default_update_url">http://eehouse.org/xw4/info.py</string>

View file

@ -1233,27 +1233,18 @@
encodings for the greater-than and less-than symbols which
are not legal in xml strings.)-->
<string name="invite_htmf">\u003ca href=\"%1$s\"\u003ETap
here\u003c/a\u003E (or the raw link below) to accept my invitation and
here\u003c/a\u003E (or the full link below) to accept my invitation and
join this game.
\u003cbr \\\u003E
\u003cbr \\\u003E
(raw link: %1$s)
\u003cbr \\\u003E
\u003cbr \\\u003E
\u003ca href=\"http://eehouse.org/market_redir.php\"\u003E Tap
here\u003c/a\u003E (or the raw link below) to
install Crosswords if you haven\'t already.
\u003cbr \\\u003E
\u003cbr \\\u003E
(raw link: http://eehouse.org/market_redir.php)
(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
@ -1440,9 +1431,7 @@
Guest wordlists; Host wins.</string>
<string name="downloading_dictf">Downloading Crosswords
wordlist %s...</string>
<string name="downloading_dictf">Downloading %s...</string>
<!--
############################################################
@ -1474,9 +1463,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,
@ -1564,8 +1554,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>
@ -2127,10 +2117,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 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>

View file

@ -299,6 +299,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 +329,6 @@
android:defaultValue="10998"
android:numeric="decimal"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_redir_host"
android:title="@string/redir_host"
android:defaultValue="@string/default_host"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_dict_host"

View file

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

View file

@ -57,7 +57,7 @@ import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
public class BoardActivity extends XWActivity
implements TransportProcs.TPMsgHandler, View.OnClickListener,
NetUtils.DownloadFinishedListener {
DictImportActivity.DownloadFinishedListener {
public static final String INTENT_KEY_CHAT = "chat";
@ -167,7 +167,7 @@ public class BoardActivity extends XWActivity
private boolean m_haveInvited = false;
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 )
@ -259,7 +259,8 @@ public class BoardActivity extends XWActivity
if ( DLG_USEDICT == id ) {
setGotGameDict( m_getDict );
} else {
NetUtils.downloadDictInBack( BoardActivity.this,
DictImportActivity
.downloadDictInBack( BoardActivity.this,
m_gi.dictLang,
m_getDict,
BoardActivity.this );
@ -1051,7 +1052,7 @@ public class BoardActivity extends XWActivity
}
//////////////////////////////////////////////////
// NetUtils.DownloadFinishedListener interface
// DictImportActivity.DownloadFinishedListener interface
//////////////////////////////////////////////////
public void downloadFinished( final String name, final boolean success )
{
@ -1731,7 +1732,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();
}
@ -1922,26 +1923,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 ) {

View file

@ -126,7 +126,7 @@ public class ConnStatusHandler {
private static HashMap<CommsConnType,SuccessRecord[]> s_records =
new HashMap<CommsConnType,SuccessRecord[]>();
private static Object s_lockObj = new Object();
private static Class s_lockObj = ConnStatusHandler.class;
private static boolean s_needsSave = false;
public static void setRect( int left, int top, int right, int bottom )

View file

@ -141,7 +141,7 @@ public class DBHelper extends SQLiteOpenHelper {
private static final String[] s_groupsSchema = {
GROUPNAME, "TEXT"
,EXPANDED, "INTEGER(0)"
,EXPANDED, "INTEGER(1)"
};
public DBHelper( Context context )

View file

@ -535,13 +535,16 @@ 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[] columns = { DBHelper.CREATE_TIME };
String selection =
String.format( "%s='%s' AND %s='%s' AND %s=%d AND %s=%d",
DBHelper.ROOMNAME, nli.room,
@ -549,9 +552,11 @@ public class DBUtils {
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();
@ -559,14 +564,14 @@ public class DBUtils {
return result;
}
public static long getRowIDForOpen( Context context, Uri data )
public static Date getMostRecentCreate( Context context, Uri data )
{
long rowid = ROWID_NOTFOUND;
NetLaunchInfo nli = new NetLaunchInfo( data );
Date result = null;
NetLaunchInfo nli = new NetLaunchInfo( context, data );
if ( null != nli && nli.isValid() ) {
rowid = getRowIDForOpen( context, nli );
result = getMostRecentCreate( context, nli );
}
return rowid;
return result;
}
public static String[] getRelayIDs( Context context, boolean noMsgs )
@ -704,11 +709,7 @@ public class DBUtils {
{
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 );
@ -718,20 +719,10 @@ public class DBUtils {
}
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();
}
setCached( rowid, null ); // force reread
updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
if ( -1 != rowid ) {
setCached( rowid, null ); // force reread
if ( -1 != rowid ) { // Is this possible? PENDING
notifyListeners( rowid );
}
return rowid;
@ -1370,25 +1361,14 @@ 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();
}
values.put( DBHelper.LASTPLAY_TIME, new Date().getTime() );
updateRow( context, DBHelper.TABLE_NAME_SUM, rowid, values );
}
private static void initDB( Context context )
@ -1401,6 +1381,23 @@ public class DBUtils {
}
}
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 )
{
synchronized( s_listeners ) {

View file

@ -1,7 +1,7 @@
/* -*- compile-command: "cd ../../../../../; ant debug install"; -*- */
/*
* Copyright 2009-2010 by Eric House (xwords@eehouse.org). All
* rights reserved.
* Copyright 2009-2012 by Eric House (xwords@eehouse.org). All rights
* reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
@ -21,6 +21,7 @@
package org.eehouse.android.xw4;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
@ -32,15 +33,37 @@ 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 {
public static final String APK_EXTRA = "APK";
// 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;
@ -50,10 +73,16 @@ public class DictImportActivity extends XWActivity {
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;
@ -71,7 +100,7 @@ public class DictImportActivity extends XWActivity {
if ( m_isApp ) {
m_appFile = saveToDownloads( is, name );
} else {
m_saved = saveDict( is, name );
m_savedDict = saveDict( is, name );
}
is.close();
} catch ( java.net.URISyntaxException use ) {
@ -89,15 +118,19 @@ public class DictImportActivity extends XWActivity {
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();
}
@ -120,24 +153,29 @@ public class DictImportActivity extends XWActivity {
Uri uri = intent.getData();
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( true );
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 ) ) {
String txt = getString( R.string.downloading_dictf,
basename( uri.getPath()) );
TextView view = (TextView)findViewById( R.id.dwnld_message );
view.setText( txt );
dft = new DownloadFilesTask( false );
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 );
}
}
@ -146,7 +184,6 @@ public class DictImportActivity extends XWActivity {
{
boolean success = false;
File appFile = new File( DictUtils.getDownloadDir( this ), name );
Assert.assertNotNull( appFile );
byte[] buf = new byte[1024*4];
try {
@ -183,6 +220,57 @@ public class DictImportActivity extends XWActivity {
{
return new File(path).getName();
}
private static void rememberListener( String url, String name,
DownloadFinishedListener lstnr )
{
ListenerData ld = new ListenerData( name, lstnr );
synchronized( s_listeners ) {
s_listeners.put( url, ld );
}
}
private static void callListener( String url, boolean success )
{
if ( null != url ) {
ListenerData ld;
synchronized( s_listeners ) {
ld = s_listeners.get( url );
if ( null != ld ) {
s_listeners.remove( url );
}
}
if ( null != ld ) {
ld.m_lstnr.downloadFinished( ld.m_dictName, success );
}
}
}
public static void downloadDictInBack( Context context, int lang,
String name,
DownloadFinishedListener lstnr )
{
String url = Utils.makeDictUrl( context, lang, name );
if ( null != lstnr ) {
rememberListener( url, name, lstnr );
}
downloadDictInBack( context, url );
}
public static void downloadDictInBack( Context context, String url )
{
Intent intent = new Intent( context, DictImportActivity.class );
intent.putExtra( DICT_EXTRA, url );
context.startActivity( intent );
}
public static Intent makeAppDownloadIntent( Context context, String url )
{
Intent intent = new Intent( context, DictImportActivity.class );
intent.putExtra( APK_EXTRA, url );
return intent;
}
}

View file

@ -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,7 +339,8 @@ public class DictsActivity extends ExpandableListActivity
String name = intent.getStringExtra( MultiService.DICT );
m_launchedForMissing = true;
m_handler = new Handler();
NetUtils.downloadDictInBack( DictsActivity.this, lang,
DictImportActivity
.downloadDictInBack( DictsActivity.this, lang,
name, DictsActivity.this );
}
};
@ -555,10 +556,9 @@ public class DictsActivity extends ExpandableListActivity
{
int loci = intent.getIntExtra( UpdateCheckReceiver.NEW_DICT_LOC, 0 );
if ( 0 < loci ) {
DictLoc loc = DictLoc.values()[loci];
String url =
intent.getStringExtra( UpdateCheckReceiver.NEW_DICT_URL );
NetUtils.downloadDictInBack( this, url, loc, null );
DictImportActivity.downloadDictInBack( this, url );
finish();
}
}
@ -769,7 +769,7 @@ public class DictsActivity extends ExpandableListActivity
launchAndDownload( activity, 0, null );
}
// NetUtils.DownloadFinishedListener interface
// DictImportActivity.DownloadFinishedListener interface
public void downloadFinished( String name, final boolean success )
{
if ( m_launchedForMissing ) {

View file

@ -33,175 +33,16 @@ 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 ) { // relay invite redirected URL case
NetLaunchInfo nli = new NetLaunchInfo( data );
if ( null != nli && nli.isValid() ) {
long rowid = DBUtils.getRowIDForOpen( this, nli );
if ( DBUtils.ROWID_NOTFOUND == rowid ) {
boolean haveDict;
if ( null == nli.dict ) { // can only test for language support
haveDict =
0 < DictLangCache.getHaveLang( this,
nli.lang ).length;
} else {
haveDict =
DictLangCache.haveDict( this, nli.lang, nli.dict );
}
if ( haveDict ) {
if ( !tryHandle( data ) ) {
mustLaunch = true;
}
} else {
Intent intent =
MultiService.makeMissingDictIntent( this, nli );
intent.putExtra( MultiService.OWNER,
MultiService.OWNER_RELAY );
// do we have gameID?
MultiService.
postMissingDictNotification( this, intent,
nli.inviteID
.hashCode() );
}
} else {
DbgUtils.logf( "DispatchNotify: dropping duplicate invite" );
GameSummary summary = DBUtils.getSummary( this, rowid );
if ( null != summary ) {
gameID = summary.gameID;
if ( !tryHandle( gameID ) ) {
mustLaunch = true;
}
}
}
}
}
if ( mustLaunch ) {
DbgUtils.logf( "DispatchNotify: nothing running" );
Intent intent = new Intent( this, GamesList.class );
// This combination of flags will bring an existing
// GamesList instance to the front, killing any children
// it has, or create a new one if none exists. Coupled
// with a "standard" launchMode it seems to work, meaning
// both that the app preserves its stack in normal use
// (you can go to Home with a stack of activities and
// return to the top activity on that stack if you
// relaunch the app) and that when I launch from here the
// stack gets nuked and we don't get a second GamesList
// instance.
intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_NEW_TASK );
if ( null != relayIDs ) {
intent.putExtra( RELAYIDS_EXTRA, relayIDs );
} else if ( -1 != gameID ) {
intent.putExtra( GAMEID_EXTRA, gameID );
} else if ( null != data ) {
intent.setData( data );
} else {
Assert.fail();
}
startActivity( intent );
if ( null != data ) { // relay invite redirected URL case
GamesList.openGame( this, data );
}
finish();
}
public static void SetRunning( Activity running )
{
if ( running instanceof HandleRelaysIface ) {
s_running.add( (HandleRelaysIface)running );
}
}
public static void ClearRunning( Activity running )
{
if ( running instanceof HandleRelaysIface ) {
s_running.remove( (HandleRelaysIface)running );
}
}
public static void SetRelayIDsHandler( HandleRelaysIface iface )
{
s_handler = iface;
}
private static boolean tryHandle( Uri data )
{
boolean handled = false;
if ( null != s_handler ) {
// This means the GamesList activity is frontmost
s_handler.handleInvite( data );
handled = true;
} else {
for ( HandleRelaysIface iface : s_running ) {
iface.handleInvite( data );
handled = true;
}
}
return handled;
}
public static boolean tryHandle( String[] relayIDs )
{
boolean handled = false;
if ( null != s_handler ) {
// This means the GamesList activity is frontmost
s_handler.handleRelaysIDs( relayIDs );
handled = true;
} else {
for ( HandleRelaysIface iface : s_running ) {
iface.handleRelaysIDs( relayIDs );
handled = true;
}
}
return handled;
}
public static boolean tryHandle( int gameID )
{
boolean handled = false;
if ( null != s_handler ) {
// This means the GamesList activity is frontmost
s_handler.handleGameID( gameID );
handled = true;
} else {
for ( HandleRelaysIface iface : s_running ) {
iface.handleGameID( gameID );
handled = true;
}
}
return handled;
}
} // onCreate
}

View file

@ -56,7 +56,18 @@ public class GCMIntentService extends GCMBaseIntentService {
@Override
protected void onMessage( Context context, Intent intent )
{
String value = intent.getStringExtra( "msg" );
String value;
value = intent.getStringExtra( "getMoves" );
if ( null != value && Boolean.parseBoolean( value ) ) {
RelayReceiver.RestartTimer( context, true );
}
value = intent.getStringExtra( "checkUpdates" );
if ( null != value && Boolean.parseBoolean( value ) ) {
UpdateCheckReceiver.checkVersions( context, true );
}
value = intent.getStringExtra( "msg" );
if ( null != value ) {
String title = intent.getStringExtra( "title" );
if ( null != title ) {
@ -64,11 +75,6 @@ public class GCMIntentService extends GCMBaseIntentService {
Utils.postNotification( context, null, title, value, code );
}
}
value = intent.getStringExtra( "getMoves" );
if ( null != value && Boolean.parseBoolean( value ) ) {
RelayReceiver.RestartTimer( context, true );
}
}
public static void init( Application app )

View file

@ -24,12 +24,16 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.Html;
import android.text.TextUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Arrays;
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;
@ -311,6 +315,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();
success = true;
@ -361,7 +366,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();
@ -555,11 +561,26 @@ 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 tmpdir = XWApp.ATTACH_SUPPORTED ?
DictUtils.getDownloadDir( context ) : null;
if ( null == tmpdir ) { // no attachment
intent.setType( "message/rfc822");
} else {
intent.setType( context.getString( R.string.invite_mime ) );
File attach = makeJsonFor( tmpdir, room, inviteID, lang,
dict, nPlayers );
Uri uri = Uri.fromFile( attach );
DbgUtils.logf( "using file uri for attachment: %s",
uri.toString() );
intent.putExtra( Intent.EXTRA_STREAM, uri );
}
choiceID = R.string.invite_chooser_email;
} else {
intent.setAction( Intent.ACTION_VIEW );
@ -724,7 +745,7 @@ public class GameUtils {
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 ) {
@ -745,6 +766,7 @@ public class GameUtils {
draw = true;
DBUtils.setMsgFlags( rowid, flags );
}
}
lock.unlock();
}
return draw;
@ -916,5 +938,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.json", room ) );
FileOutputStream fos = new FileOutputStream( file );
fos.write( data, 0, data.length );
fos.close();
result = file;
} catch ( Exception ex ) {
DbgUtils.loge( ex );
}
}
return result;
}
}

View file

@ -46,17 +46,18 @@ import android.widget.ExpandableListView;
import android.widget.LinearLayout;
import android.widget.ListView;
import java.io.File;
import java.util.Date;
// import android.telephony.PhoneStateListener;
// import android.telephony.TelephonyManager;
import junit.framework.Assert;
import org.eehouse.android.xw4.jni.*;
public class GamesList extends XWExpandableListActivity
implements DispatchNotify.HandleRelaysIface,
DBUtils.DBChangeListener,
public class GamesList extends XWListActivity
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;
@ -66,11 +67,15 @@ public class GamesList extends XWExpandableListActivity
private static final int NEW_GROUP = WARN_NODICT + 5;
private static final int RENAME_GROUP = WARN_NODICT + 6;
private static final int CHANGE_GROUP = WARN_NODICT + 7;
private static final int WARN_NODICT_NEW = WARN_NODICT + 8;
private static final String SAVE_ROWID = "SAVE_ROWID";
private static final String SAVE_GROUPID = "SAVE_GROUPID";
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 int NEW_NET_GAME_ACTION = 1;
private static final int RESET_GAME_ACTION = 2;
private static final int DELETE_GAME_ACTION = 3;
@ -89,7 +94,7 @@ public class GamesList extends XWExpandableListActivity
private GameListAdapter m_adapter;
private String m_missingDict;
private String m_missingDictName;
private long m_missingDictRowId;
private long m_missingDictRowId = DBUtils.ROWID_NOTFOUND;
private String[] m_sameLangDicts;
private int m_missingDictLang;
private long m_rowid;
@ -111,15 +116,17 @@ public class GamesList extends XWExpandableListActivity
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
// no name, so user must pick
if ( null == m_missingDictName ) {
DictsActivity.launchAndDownload( GamesList.this,
m_missingDictLang );
} else {
NetUtils.downloadDictInBack( GamesList.this,
DictImportActivity
.downloadDictInBack( GamesList.this,
m_missingDictLang,
m_missingDictName,
GamesList.this );
@ -133,6 +140,10 @@ public class GamesList extends XWExpandableListActivity
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_missingDictName,
@ -142,7 +153,7 @@ public class GamesList extends XWExpandableListActivity
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 ) {
@ -170,8 +181,7 @@ public class GamesList extends XWExpandableListActivity
m_missingDictRowId,
m_missingDictName,
dict );
GameUtils.launchGame( GamesList.this,
m_missingDictRowId );
launchGameIf();
}
};
dialog = new AlertDialog.Builder( this )
@ -355,8 +365,7 @@ public class GamesList extends XWExpandableListActivity
{
super.onNewIntent( intent );
Assert.assertNotNull( intent );
invalRelayIDs( intent.
getStringArrayExtra( DispatchNotify.RELAYIDS_EXTRA ) );
invalRelayIDs( intent.getStringArrayExtra( RELAYIDS_EXTRA ) );
startFirstHasDict( intent );
startNewNetGame( intent );
startHasGameID( intent );
@ -366,7 +375,6 @@ public class GamesList extends XWExpandableListActivity
protected void onStart()
{
super.onStart();
DispatchNotify.SetRelayIDsHandler( this );
boolean hide = CommonPrefs.getHideIntro( this );
int hereOrGone = hide ? View.GONE : View.VISIBLE;
@ -393,7 +401,6 @@ public class GamesList extends XWExpandableListActivity
// (TelephonyManager)getSystemService( Context.TELEPHONY_SERVICE );
// mgr.listen( m_phoneStateListener, PhoneStateListener.LISTEN_NONE );
// m_phoneStateListener = null;
DispatchNotify.SetRelayIDsHandler( null );
super.onStop();
}
@ -436,39 +443,6 @@ public class GamesList extends XWExpandableListActivity
}
}
// 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 )
{
@ -538,8 +512,9 @@ public class GamesList extends XWExpandableListActivity
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 );
@ -724,15 +699,21 @@ public class GamesList extends XWExpandableListActivity
return handled;
}
// NetUtils.DownloadFinishedListener interface
// DictImportActivity.DownloadFinishedListener interface
public void downloadFinished( String name, final boolean success )
{
post( new Runnable() {
public void run() {
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 );
}
}
} );
}
@ -833,13 +814,38 @@ public class GamesList extends XWExpandableListActivity
return handled;
}
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_missingDictLang = missingLang[0];
if ( 0 < missingNames[0].length ) {
@ -855,7 +861,7 @@ public class GamesList extends XWExpandableListActivity
} else {
String dict = DictLangCache.getHaveLang( this, m_missingDictLang)[0];
GameUtils.replaceDicts( this, m_missingDictRowId, null, dict );
GameUtils.launchGame( this, m_missingDictRowId );
launchGameIf();
}
}
return hasDicts;
@ -899,8 +905,7 @@ public class GamesList extends XWExpandableListActivity
private void startFirstHasDict( Intent intent )
{
if ( null != intent ) {
String[] relayIDs =
intent.getStringArrayExtra( DispatchNotify.RELAYIDS_EXTRA );
String[] relayIDs = intent.getStringArrayExtra( RELAYIDS_EXTRA );
startFirstHasDict( relayIDs );
}
}
@ -910,33 +915,35 @@ public class GamesList extends XWExpandableListActivity
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 )
{
NetLaunchInfo info = null;
NetLaunchInfo nli = null;
if ( MultiService.isMissingDictIntent( intent ) ) {
info = new NetLaunchInfo( intent );
nli = new NetLaunchInfo( intent );
} else {
Uri data = intent.getData();
if ( null != data ) {
info = new NetLaunchInfo( data );
nli = new NetLaunchInfo( this, data );
}
}
if ( null != info && info.isValid() ) {
startNewNetGame( info );
if ( null != nli && nli.isValid() ) {
startNewNetGame( nli );
}
} // startNewNetGame
@ -950,7 +957,7 @@ public class GamesList extends XWExpandableListActivity
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 );
}
@ -992,9 +999,65 @@ public class GamesList extends XWExpandableListActivity
return dialog;
}
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 makeNewNetGame( NetLaunchInfo info )
{
long rowid = GameUtils.makeNewNetGame( this, info );
GameUtils.launchGame( this, 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 void openGame( Context context, Uri data )
{
Intent intent = makeSelfIntent( context );
intent.setData( data );
context.startActivity( intent );
}
}

View file

@ -20,12 +20,15 @@
package org.eehouse.android.xw4;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri.Builder;
import android.net.Uri;
import android.os.Bundle;
import java.net.URLEncoder;
import java.io.InputStream;
import org.json.JSONObject;
import junit.framework.Assert;
public class NetLaunchInfo {
@ -64,11 +67,27 @@ public class NetLaunchInfo {
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 {
if ( "content".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" );
@ -76,6 +95,7 @@ public class NetLaunchInfo {
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() );
@ -99,15 +119,15 @@ public class NetLaunchInfo {
String inviteID, int lang,
String dict, int nPlayersT )
{
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", nPlayersT ) );
ub.appendQueryParameter( "room", room );
ub.appendQueryParameter( "id", inviteID );
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 );
}

View file

@ -21,17 +21,11 @@
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;
@ -50,10 +44,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 )
{
@ -273,68 +263,4 @@ public class NetUtils {
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 );
} catch ( java.net.MalformedURLException mue ) {
DbgUtils.loge( mue );
} catch ( java.io.IOException ioe ) {
DbgUtils.loge( ioe );
} catch ( Exception ce ) {
// E.g. java.net.ConnectException; we failed
// to download, ok.
} finally {
if ( null != urlConn ) {
urlConn.disconnect();
}
}
sno.close();
if ( success ) {
DictLangCache.inval( context, name, loc, true );
}
if ( null != lstnr ) {
lstnr.downloadFinished( name, success );
}
}
} ).start();
}
}

View file

@ -63,13 +63,13 @@ 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,
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 );
}
}
}
@ -112,9 +112,7 @@ public class RelayService extends Service {
if ( 0 < idsWMsgs.size() ) {
String[] relayIDs = new String[idsWMsgs.size()];
idsWMsgs.toArray( relayIDs );
// if ( !DispatchNotify.tryHandle( relayIDs ) ) {
setupNotification( relayIDs );
// }
}
sink.send( this );
}

View file

@ -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;
@ -432,7 +430,8 @@ public class SMSService extends Service {
MultiService.OWNER_SMS );
intent.putExtra( MultiService.INVITER,
Utils.phoneToContact( this, phone, true ) );
MultiService.postMissingDictNotification( this, intent, gameID );
MultiService.postMissingDictNotification( this, intent,
gameID );
}
break;
case DATA:
@ -504,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) );
@ -542,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 );
}
@ -563,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 );
@ -607,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
@ -664,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!!!" );
@ -688,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 {
@ -709,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 {
@ -752,7 +744,6 @@ public class SMSService extends Service {
public boolean isComplete()
{
boolean complete = m_msgs.length == m_haveCount;
DbgUtils.logf( "isComplete(msg %d)=>%b", m_msgID, complete );
return complete;
}

View file

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

View file

@ -197,9 +197,8 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
intent = new Intent( Intent.ACTION_VIEW,
Uri.parse(url) );
} else {
intent = new Intent( context,
DictImportActivity.class );
intent.putExtra( DictImportActivity.APK_EXTRA, url );
intent = DictImportActivity
.makeAppDownloadIntent( context, url );
}
String title =

View file

@ -174,7 +174,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
@ -425,7 +426,8 @@ public class Utils {
public static Intent makeInstallIntent( File file )
{
Uri uri = Uri.parse( "file:/" + file.getPath() );
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 );
@ -442,7 +444,6 @@ public class Utils {
pm.queryIntentActivities( intent,
PackageManager.MATCH_DEFAULT_ONLY );
result = 0 < doers.size();
DbgUtils.logf( "canInstall()=>%b", result );
return result;
}

View file

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

View file

@ -32,6 +32,7 @@ public class XWApp extends Application {
public static final boolean BTSUPPORTED = false;
public static final boolean SMSSUPPORTED = true;
public static final boolean GCMSUPPORTED = true;
public static final boolean ATTACH_SUPPORTED = false;
public static final boolean DEBUG = true;
public static final String SMS_PUBLIC_HEADER = "-XW4";

View file

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

View file

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

View file

@ -0,0 +1,106 @@
<?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 script to recognize"
. " this as an Android browser.");
print <<<EOF
<div class="center">
<p>This page is meant to be viewed on an Android device.</p>
<hr>
<p>(If you <em>are</em> viewing this on an Android device, you&apos;ve
found a bug! Please <a href="mailto:
xwords@eehouse.org?subject=$subject&body=$body">email me</a> (and be
sure to leave the user agent string in the email body.)
</p>
</div>
EOF;
}
function printAndroid() {
print <<<EOF
<div>
<p>You&apos;ll have come here after clicking a link in an email or
text inviting you to a Crosswords game. But you should not be seeing
this page.</p>
<p>If you got this page on your device, it means either
<ul>
<li>The copy of Crosswords you have is NOT beta 56 or newer (dating from about Dec. 1, 2012).</li>
<li> OR </li>
<li> that your copy of Crosswords is new enough <em>BUT</em> that
when you clicked on the link and were asked to choose between a
browser and Crosswords you chose the browser.</li>
</ul></p>
<p>In the first case, install the latest Crosswords,
either <a href="market://search?q=pname:org.eehouse.android.xw4">via
the Google Play store</a> or
(sideloading) <a href="https://sourceforge.net/projects/xwords/files/xwords_Android/4.4%20beta%2056/XWords4-release_android_beta_56.apk/download">via
Sourceforge.net</a>. After the install is finished go back to the
invite email (or text) and tap the link again.</p>
<p>In the second case, hit your browser&apos;s back button, click the
link in your invite email (or text) again, and this time let
Crosswords handle it.</p>
<p>Have fun. And as always, <a href="mailto:xwords@eehouse.org">let
me know</a> if you have problems or suggestions.</p>
</div>
<div class="center">
<img class="center" src="../icon48x48.png"/>
</div>
EOF;
}
/**********************************************************************
* Main()
**********************************************************************/
$agent = $_SERVER['HTTP_USER_AGENT'];
$onAndroid = false;
for ( $ii = 0; $ii < count($g_androidStrings) && !$onAndroid; ++$ii ) {
$needle = $g_androidStrings[$ii];
$onAndroid = false !== stripos( $agent, $needle );
}
$onFire = false !== stripos( $agent, 'silk' );
printHead();
if ( /*true || */ $onFire || $onAndroid ) {
printAndroid();
} else {
printNonAndroid($agent);
}
printTail();
?>

View file

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

View file

@ -45,7 +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_SENDTIME
# CPPFLAGS += -DHAVE_STIME
# turn on semaphore debugging
# CPPFLAGS += -DDEBUG_LOCKS

View file

@ -581,8 +581,8 @@ DBMgr::PendingMsgCount( const char* connName, int hid )
int count = 0;
const char* fmt = "SELECT COUNT(*) FROM " MSGS_TABLE
" WHERE connName = '%s' AND hid = %d "
#ifdef HAVE_SENDTIME
"AND sendtime IS NULL"
#ifdef HAVE_STIME
"AND stime IS NULL"
#endif
;
string query;
@ -692,8 +692,8 @@ DBMgr::CountStoredMessages( const char* const connName, int hid )
{
const char* fmt = "SELECT count(*) FROM " MSGS_TABLE
" WHERE connname = '%s' "
#ifdef HAVE_SENDTIME
"AND sendtime IS NULL"
#ifdef HAVE_STIME
"AND stime IS NULL"
#endif
;
@ -748,8 +748,8 @@ DBMgr::GetNthStoredMessage( const char* const connName, int hid,
{
const char* fmt = "SELECT id, msg, msglen FROM " MSGS_TABLE
" WHERE connName = '%s' AND hid = %d "
#ifdef HAVE_SENDTIME
"AND sendtime IS NULL "
#ifdef HAVE_STIME
"AND stime IS NULL "
#endif
"ORDER BY id LIMIT 1 OFFSET %d";
string query;
@ -807,8 +807,8 @@ DBMgr::RemoveStoredMessages( const int* msgIDs, int nMsgIDs )
}
const char* fmt =
#ifdef HAVE_SENDTIME
"UPDATE " MSGS_TABLE " SET sendtime='now' "
#ifdef HAVE_STIME
"UPDATE " MSGS_TABLE " SET stime='now' "
#else
"DELETE FROM " MSGS_TABLE
#endif

View file

@ -50,10 +50,12 @@ def init():
return con
# WHERE stime IS NULL
def getPendingMsgs( con, typ ):
cur = con.cursor()
query = """SELECT id, devid FROM msgs WHERE
devid IN (SELECT id FROM devices WHERE devtype=%d)
query = """SELECT id, devid FROM msgs
WHERE devid IN (SELECT id FROM devices WHERE devtype=%d)
AND NOT connname IN (SELECT connname FROM games WHERE dead); """
cur.execute(query % typ)
result = cur.fetchall()
@ -75,23 +77,28 @@ def notifyGCM( devids, typ ):
# 'msg' : "I am your father, Luke.",
}
response = instance.json_request( registration_ids = devids,
data = data )
# restricted_package_name = 'org.eehouse.android.xw4',
data = data,
# collapse_key = 'NewMove',
)
if 'errors' in response:
for error, reg_ids in response.items():
print error
response = response['errors']
if 'NotRegistered' in response:
for id in response['NotRegistered']:
print 'need to remove "', id, '" from db'
else:
print 'no errors',
if g_debug: print ':', response
else: print
print "got some kind of error"
else:
if g_debug: print 'no errors:', response
else:
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
@ -99,7 +106,6 @@ def shouldSend(val):
# before, backoff applies.
def targetsAfterBackoff( msgs ):
global g_sent
print 'sent:', g_sent
targets = {}
for row in msgs:
msgid = row[0]
@ -174,16 +180,16 @@ def main():
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
notifyGCM( asGCMIds( g_con, targets, typ ), typ )
pruneSent( devids )
else:
emptyCount += 1
if not g_debug:
if (0 == (emptyCount%5)) and not g_debug:
sys.stdout.write('.')
sys.stdout.flush()
if 0 == (emptyCount % LINE_LEN): print ""
if 0 == (emptyCount % (LINE_LEN*5)): print ""
if 0 == loopInterval: break
time.sleep( loopInterval )

View file

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

View file

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

View file

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