Merge remote branch 'origin/android_localize' into android_localize

This commit is contained in:
Eric House 2014-04-29 06:39:35 -07:00
commit b4d5130588
32 changed files with 1532 additions and 1031 deletions

View file

@ -97,7 +97,7 @@
<activity android:name="SMSInviteActivity"
android:theme="@android:style/Theme.Dialog"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="portrait"
android:screenOrientation="sensor"
/>
<activity android:name="GameConfigActivity"
@ -208,6 +208,14 @@
<intent-filter android:priority="999" >
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DATA_SMS_RECEIVED" />
<data android:scheme="sms" />
<data android:port="3344" />
<data android:host="*" />
</intent-filter>
</receiver>
<service android:name="SMSService"/>

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
<h3>New with this release</h3>
<ul>
<li>Display board in upside-down portrait orientation too (where OS version allows)</li>
<li>Bug fix (obscure): don't mangle unicode model names</li>
</ul>

View file

@ -41,6 +41,7 @@
<string name="key_dict_host">key_dict_host3</string>
<string name="key_logging_on">key_logging_on</string>
<string name="key_show_sms">key_show_sms</string>
<string name="key_send_data_sms">key_send_data_sms</string>
<string name="key_init_hintsallowed">key_init_hintsallowed</string>
<string name="key_init_nethintsallowed">key_init_nethintsallowed</string>
<string name="key_init_autojuggle">key_init_autojuggle</string>
@ -146,6 +147,8 @@
<string name="debug_features">Enable debug features</string>
<string name="debug_features_summary">Menuitems etc. (release builds
only)</string>
<string name="title_send_data_sms">Send SMS as data</string>
<string name="summary_send_data_sms">(GSM phones only)</string>
<string name="board_menu_game_netstats">Network stats</string>
<string name="netstats_title">Game network stats</string>

View file

@ -886,7 +886,7 @@
<string name="summary_field">Include in game listing</string>
<!-- Put nothing in the summary space, so it just reads "Game 2" -->
<string name="game_summary_field_empty">\u003cNothing\u003E</string>
<string name="game_summary_field_empty">\u003CNothing\u003E</string>
<!-- Put the language there, so it reads "Game 2 (English)" -->
<string name="game_summary_field_language">Game language#1</string>
<!-- List names of opponents (summarized), e.g. "Game 2 (vs Kati)" -->
@ -1200,13 +1200,13 @@
substituted for "%1$s". (The funky \u003c and friends are
encodings for the greater-than and less-than symbols which
are not legal in xml strings.)-->
<string name="invite_htm_fmt">\u003ca href=\"%1$s\"\u003ETap
here\u003c/a\u003E (or tap the full link below, or, if you already
<string name="invite_htm_fmt">\u003Ca href=\"%1$s\"\u003ETap
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
\u003Cbr \\\u003E
\u003Cbr \\\u003E
(full link: %1$s )
</string>
@ -2235,5 +2235,7 @@
<string name="xlations_enabled_title">Enable local translating</string>
<string name="xlations_enabled_summary">Add option to every screen menu</string>
<string name="data_gsm_only">SMS Data is only available on GSM phones.</string>
</resources>

View file

@ -344,6 +344,11 @@
android:title="Show SMS sends, receives"
android:defaultValue="false"
/>
<CheckBoxPreference android:key="@string/key_send_data_sms"
android:title="@string/title_send_data_sms"
android:summary="@string/summary_send_data_sms"
android:defaultValue="false"
/>
<org.eehouse.android.xw4.XWEditTextPreference
android:key="@string/key_relay_host"
android:title="@string/relay_host"

View file

@ -21,68 +21,17 @@
package org.eehouse.android.xw4;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface.OnDismissListener;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashSet;
import java.util.concurrent.Semaphore;
import junit.framework.Assert;
import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.*;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.jni.CurGameInfo.DeviceRole;
import org.eehouse.android.xw4.jni.JNIThread.*;
public class BoardActivity extends Activity {
public static final String INTENT_KEY_CHAT = "chat";
private static final int CHAT_REQUEST = 1;
private static final int BT_INVITE_RESULT = 2;
private static final int SMS_INVITE_RESULT = 3;
private static final int SCREEN_ON_TIME = 10 * 60 * 1000; // 10 mins
private static final String DLG_TITLE = "DLG_TITLE";
private static final String DLG_TITLESTR = "DLG_TITLESTR";
private static final String DLG_BYTES = "DLG_BYTES";
private static final String ROOM = "ROOM";
private static final String PWDNAME = "PWDNAME";
private static final String TOASTSTR = "TOASTSTR";
private static final String WORDS = "WORDS";
private static final String GETDICT = "GETDICT";
private BoardDelegate m_dlgt;
@Override
@ -90,7 +39,7 @@ public class BoardActivity extends Activity {
{
Dialog dialog = super.onCreateDialog( id );
if ( null == dialog ) {
dialog = m_dlgt.createDialog( id );
dialog = m_dlgt.onCreateDialog( id );
}
return dialog;
} // onCreateDialog
@ -106,6 +55,12 @@ public class BoardActivity extends Activity {
protected void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
if ( 9 <= Integer.valueOf( android.os.Build.VERSION.SDK ) ) {
setRequestedOrientation( ActivityInfo.
SCREEN_ORIENTATION_SENSOR_PORTRAIT );
}
m_dlgt = new BoardDelegate( this, savedInstanceState );
m_dlgt.init( savedInstanceState );
} // onCreate

View file

@ -236,9 +236,9 @@ public class BoardDelegate extends DelegateBase
}
}
protected Dialog createDialog( int id )
protected Dialog onCreateDialog( int id )
{
Dialog dialog = super.createDialog( id );
Dialog dialog = super.onCreateDialog( id );
if ( null == dialog ) {
DialogInterface.OnClickListener lstnr;
AlertDialog.Builder ab = makeAlertBuilder();

View file

@ -101,7 +101,7 @@ public class ChatDelegate extends DelegateBase
DBUtils.appendChatHistory( m_activity, m_rowid, text, true );
Intent result = new Intent();
result.putExtra( BoardActivity.INTENT_KEY_CHAT, text );
result.putExtra( BoardDelegate.INTENT_KEY_CHAT, text );
m_activity.setResult( Activity.RESULT_OK, result );
}
finish();

View file

@ -122,7 +122,7 @@ public class DelegateBase implements DlgDelegate.DlgClickNotify,
m_delegate.showDialog( dlgID );
}
protected Dialog createDialog( int id )
protected Dialog onCreateDialog( int id )
{
return m_delegate.createDialog( id );
}

View file

@ -87,7 +87,7 @@ public class DictsActivity extends ExpandableListActivity {
{
Dialog dialog = super.onCreateDialog( id );
if ( null == dialog ) {
dialog = m_dlgt.createDialog( id );
dialog = m_dlgt.onCreateDialog( id );
}
return dialog;
} // onCreateDialog

View file

@ -234,7 +234,7 @@ public class DictsDelegate extends DelegateBase
m_activity = activity;
}
protected Dialog createDialog( int id )
protected Dialog onCreateDialog( int id )
{
OnClickListener lstnr, lstnr2;
Dialog dialog;
@ -341,7 +341,7 @@ public class DictsDelegate extends DelegateBase
break;
default:
dialog = super.createDialog( id );
dialog = super.onCreateDialog( id );
doRemove = false;
break;
}
@ -351,7 +351,7 @@ public class DictsDelegate extends DelegateBase
}
return dialog;
} // createDialog
} // onCreateDialog
protected void prepareDialog( int id, Dialog dialog )
{

View file

@ -48,7 +48,7 @@ public class GamesListActivity extends ListActivity {
{
Dialog dialog = super.onCreateDialog( id );
if ( null == dialog && null != m_dlgt ) {
dialog = m_dlgt.createDialog( id );
dialog = m_dlgt.onCreateDialog( id );
}
return dialog;
} // onCreateDialog

View file

@ -127,7 +127,7 @@ public class GamesListDelegate extends DelegateBase
m_activity = activity;
}
protected Dialog createDialog( int id )
protected Dialog onCreateDialog( int id )
{
Dialog dialog = null;
DialogInterface.OnClickListener lstnr;
@ -331,7 +331,7 @@ public class GamesListDelegate extends DelegateBase
break;
default:
dialog = super.createDialog( id );
dialog = super.onCreateDialog( id );
break;
}
return dialog;

View file

@ -25,6 +25,8 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import org.eehouse.android.xw4.loc.LocUtils;
public class PrefsActivity extends PreferenceActivity {
private PrefsDelegate m_dlgt;
@ -38,12 +40,18 @@ public class PrefsActivity extends PreferenceActivity {
@Override
protected void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
m_dlgt = new PrefsDelegate( this, savedInstanceState );
super.onCreate( savedInstanceState );
m_dlgt.init( savedInstanceState );
}
@Override
protected void onStart()
{
LocUtils.xlatePreferences( this );
super.onStart();
}
@Override
protected void onResume()
{

View file

@ -42,6 +42,7 @@ public class PrefsDelegate extends DelegateBase
private String m_keyLogging;
private String m_smsToasting;
private String m_smsEnable;
private String m_smsData;
private String m_downloadPath;
private String m_thumbSize;
private String m_hideTitle;
@ -55,71 +56,73 @@ public class PrefsDelegate extends DelegateBase
protected Dialog onCreateDialog( int id )
{
Dialog dialog = null;
DialogInterface.OnClickListener lstnr = null;
int confirmID = 0;
Dialog dialog = super.onCreateDialog( id );
if ( null == dialog ) {
DialogInterface.OnClickListener lstnr = null;
int confirmID = 0;
switch( DlgID.values()[id] ) {
case REVERT_COLORS:
confirmID = R.string.confirm_revert_colors;
lstnr = new DialogInterface.OnClickListener() {
public void onClick( DialogInterface dlg, int item ) {
SharedPreferences sp = getSharedPreferences();
SharedPreferences.Editor editor = sp.edit();
int[] colorKeys = {
R.string.key_player0,
R.string.key_player1,
R.string.key_player2,
R.string.key_player3,
R.string.key_bonus_l2x,
R.string.key_bonus_l3x,
R.string.key_bonus_w2x,
R.string.key_bonus_w3x,
R.string.key_tile_back,
R.string.key_clr_crosshairs,
R.string.key_empty,
R.string.key_background,
R.string.key_clr_bonushint,
};
for ( int colorKey : colorKeys ) {
editor.remove( getString(colorKey) );
switch( DlgID.values()[id] ) {
case REVERT_COLORS:
confirmID = R.string.confirm_revert_colors;
lstnr = new DialogInterface.OnClickListener() {
public void onClick( DialogInterface dlg, int item ) {
SharedPreferences sp = getSharedPreferences();
SharedPreferences.Editor editor = sp.edit();
int[] colorKeys = {
R.string.key_player0,
R.string.key_player1,
R.string.key_player2,
R.string.key_player3,
R.string.key_bonus_l2x,
R.string.key_bonus_l3x,
R.string.key_bonus_w2x,
R.string.key_bonus_w3x,
R.string.key_tile_back,
R.string.key_clr_crosshairs,
R.string.key_empty,
R.string.key_background,
R.string.key_clr_bonushint,
};
for ( int colorKey : colorKeys ) {
editor.remove( getString(colorKey) );
}
editor.commit();
relaunch();
}
editor.commit();
relaunch();
}
};
break;
case REVERT_ALL:
confirmID = R.string.confirm_revert_all;
lstnr = new DialogInterface.OnClickListener() {
public void onClick( DialogInterface dlg, int item ) {
SharedPreferences sp = getSharedPreferences();
SharedPreferences.Editor editor = sp.edit();
editor.clear();
editor.commit();
relaunch();
}
};
break;
case CONFIRM_SMS:
dialog = SMSCheckBoxPreference.onCreateDialog( m_activity, id );
break;
case EXPLAIN_TITLE:
dialog = LocUtils.makeAlertBuilder( m_activity )
.setMessage( R.string.no_hide_titlebar )
.setTitle( R.string.info_title )
.setPositiveButton( R.string.button_ok, null )
.create();
break;
}
};
break;
case REVERT_ALL:
confirmID = R.string.confirm_revert_all;
lstnr = new DialogInterface.OnClickListener() {
public void onClick( DialogInterface dlg, int item ) {
SharedPreferences sp = getSharedPreferences();
SharedPreferences.Editor editor = sp.edit();
editor.clear();
editor.commit();
relaunch();
}
};
break;
case CONFIRM_SMS:
dialog = SMSCheckBoxPreference.onCreateDialog( m_activity, id );
break;
case EXPLAIN_TITLE:
dialog = LocUtils.makeAlertBuilder( m_activity )
.setMessage( R.string.no_hide_titlebar )
.setTitle( R.string.info_title )
.setPositiveButton( R.string.button_ok, null )
.create();
break;
}
if ( null == dialog && null != lstnr ) {
dialog = LocUtils.makeAlertBuilder( m_activity )
.setTitle( R.string.query_title )
.setMessage( confirmID )
.setPositiveButton( R.string.button_ok, lstnr )
.setNegativeButton( R.string.button_cancel, null )
.create();
if ( null == dialog && null != lstnr ) {
dialog = LocUtils.makeAlertBuilder( m_activity )
.setTitle( R.string.query_title )
.setMessage( confirmID )
.setPositiveButton( R.string.button_ok, lstnr )
.setNegativeButton( R.string.button_cancel, null )
.create();
}
}
return dialog;
}
@ -133,6 +136,7 @@ public class PrefsDelegate extends DelegateBase
m_keyLogging = getString( R.string.key_logging_on );
m_smsToasting = getString( R.string.key_show_sms );
m_smsEnable = getString( R.string.key_enable_sms );
m_smsData = getString( R.string.key_send_data_sms );
m_downloadPath = getString( R.string.key_download_path );
m_thumbSize = getString( R.string.key_thumbsize );
m_hideTitle = getString( R.string.key_hide_title );
@ -165,6 +169,7 @@ public class PrefsDelegate extends DelegateBase
@Override
public void onSharedPreferenceChanged( SharedPreferences sp, String key )
{
// DbgUtils.logf( "onSharedPreferenceChanged(key=%s)", key );
if ( key.equals( m_keyLogging ) ) {
DbgUtils.logEnable( sp.getBoolean( key, false ) );
} else if ( key.equals( m_smsToasting ) ) {
@ -176,6 +181,13 @@ public class PrefsDelegate extends DelegateBase
SMSService.stopService( m_activity );
XWPrefs.setHaveCheckedSMS( m_activity, false );
}
} else if ( key.equals( m_smsData ) ) {
boolean turningOn = sp.getBoolean( key, true );
if ( turningOn && !Utils.isGSMPhone( m_activity ) ) {
showOKOnlyDialog( R.string.data_gsm_only );
((CheckBoxPreference)(m_activity.findPreference( key )))
.setChecked( false );
}
} else if ( key.equals( m_downloadPath ) ) {
String value = sp.getString( key, null );
if ( null != value ) {

View file

@ -33,28 +33,37 @@ public class SMSReceiver extends BroadcastReceiver {
@Override
public void onReceive( Context context, Intent intent )
{
String action = intent.getAction();
Bundle bundle = intent.getExtras();
DbgUtils.logf( "onReceive: action=%s", action );
if ( null != bundle ) {
boolean isData =
action.equals("android.intent.action.DATA_SMS_RECEIVED");
boolean isMine = false;
Object[] pdus = (Object[])bundle.get( "pdus" );
SmsMessage[] smses = new SmsMessage[pdus.length];
for ( int ii = 0; ii < pdus.length; ++ii ) {
SmsMessage sms = SmsMessage.createFromPdu((byte[])pdus[ii]);
String body = sms.getMessageBody();
String postDetectable = SMSService.fromPublicFmt( body );
isMine = null != postDetectable;
if ( isMine ) {
String phone = sms.getOriginatingAddress();
DbgUtils.logf( "SMSReceiver: \"%s\" from %s",
body, phone );
SMSService.handleFrom( context, postDetectable, phone );
String phone = sms.getOriginatingAddress();
if ( isData ) {
byte[] body = sms.getUserData();
SMSService.handleFrom( context, body, phone );
} else {
String body = sms.getMessageBody();
String postDetectable = SMSService.fromPublicFmt( body );
isMine = null != postDetectable;
if ( isMine ) {
DbgUtils.logf( "SMSReceiver: \"%s\" from %s",
body, phone );
SMSService.handleFrom( context, postDetectable, phone );
}
}
}
if ( isMine ) {
DbgUtils.logf( "SMSReceiver: CONSUMING message" );
abortBroadcast();
if ( isMine ) {
DbgUtils.logf( "SMSReceiver: CONSUMING message" );
abortBroadcast();
}
}
}
}

View file

@ -29,6 +29,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
@ -62,6 +63,7 @@ public class SMSService extends XWService {
private static final int SMS_PROTO_VERSION = 0;
private static final int MAX_LEN_TEXT = 100;
private static final int MAX_LEN_BINARY = 100;
private static final int HANDLE = 1;
private static final int INVITE = 2;
private static final int SEND = 3;
@ -70,6 +72,7 @@ public class SMSService extends XWService {
private static final int CHECK_MSGDB = 6;
private static final int ADDED_MISSING = 7;
private static final int STOP_SELF = 8;
private static final int HANDLEDATA = 9;
private static final String CMD_STR = "CMD";
private static final String BUFFER = "BUFFER";
@ -77,13 +80,15 @@ public class SMSService extends XWService {
private static final String PHONE = "PHONE";
private static Boolean s_showToasts = null;
private static Boolean s_asData = null;
// All messages are base64-encoded byte arrays. The first byte is
// always one of these. What follows depends.
private enum SMS_CMD { NONE, INVITE, DATA, DEATH, ACK, };
private BroadcastReceiver m_sentReceiver;
private BroadcastReceiver m_receiveReceiver;
private OnSharedPreferenceChangeListener m_prefsListener;
private int m_nReceived = 0;
private static int s_nSent = 0;
@ -119,6 +124,15 @@ public class SMSService extends XWService {
context.startService( intent );
}
public static void handleFrom( Context context, byte[] buffer,
String phone )
{
Intent intent = getIntentTo( context, HANDLEDATA );
intent.putExtra( BUFFER, buffer );
intent.putExtra( PHONE, phone );
context.startService( intent );
}
public static void inviteRemote( Context context, String phone,
int gameID, String gameName,
int lang, String dict,
@ -201,10 +215,8 @@ public class SMSService extends XWService {
private static Intent getIntentTo( Context context, int cmd )
{
if ( null == s_showToasts ) {
SharedPreferences sp
= PreferenceManager.getDefaultSharedPreferences( context );
String key = LocUtils.getString( context, R.string.key_show_sms );
s_showToasts = sp.getBoolean( key, false );
s_showToasts =
XWPrefs.getPrefsBoolean( context, R.string.key_show_sms, false );
}
Intent intent = new Intent( context, SMSService.class );
@ -233,6 +245,13 @@ public class SMSService extends XWService {
unregisterReceiver( m_receiveReceiver );
m_receiveReceiver = null;
}
if ( null != m_prefsListener ) {
SharedPreferences sp
= PreferenceManager.getDefaultSharedPreferences( this );
sp.unregisterOnSharedPreferenceChangeListener( m_prefsListener );
m_prefsListener = null;
}
super.onDestroy();
}
@ -257,6 +276,7 @@ public class SMSService extends XWService {
}
break;
case HANDLE:
case HANDLEDATA:
++m_nReceived;
ConnStatusHandler.
updateStatusIn( this, null,
@ -264,9 +284,14 @@ public class SMSService extends XWService {
if ( s_showToasts ) {
DbgUtils.showf( this, "got %dth msg", m_nReceived );
}
String buffer = intent.getStringExtra( BUFFER );
String phone = intent.getStringExtra( PHONE );
receiveBuffer( buffer, phone );
if ( HANDLE == cmd ) {
String buffer = intent.getStringExtra( BUFFER );
receiveBuffer( buffer, phone );
} else {
byte[] buffer = intent.getByteArrayExtra( BUFFER );
receiveBuffer( buffer, phone );
}
break;
case INVITE:
case ADDED_MISSING:
@ -388,13 +413,24 @@ public class SMSService extends XWService {
das.write( bytes, 0, bytes.length );
das.flush();
String as64 = XwJNI.base64Encode( bas.toByteArray() );
String[] msgs = breakAndEncode( as64 );
return sendBuffers( msgs, phone );
byte[] data = bas.toByteArray();
boolean result;
if ( null == s_asData ) {
boolean asData =
XWPrefs.getPrefsBoolean( this, R.string.key_send_data_sms,
true );
s_asData = new Boolean( asData );
}
if ( s_asData ) {
byte[][] msgs = breakAndEncode( data );
result = sendBuffers( msgs, phone, data );
} else {
result = sendAsText( data, phone );
}
return result;
}
private String[] breakAndEncode( String msg )
throws java.io.IOException
private String[] breakAndEncode( String msg ) throws java.io.IOException
{
// TODO: as optimization, truncate header when only one packet
// required
@ -418,6 +454,33 @@ public class SMSService extends XWService {
return result;
}
private byte[][] breakAndEncode( byte msg[] ) throws java.io.IOException
{
int count = (msg.length + (MAX_LEN_BINARY-1)) / MAX_LEN_BINARY;
byte[][] result = new byte[count][];
int msgID = ++s_nSent % 0x000000FF;
int start = 0;
int end = 0;
for ( int ii = 0; ii < count; ++ii ) {
int len = msg.length - end;
if ( len > MAX_LEN_BINARY ) {
len = MAX_LEN_BINARY;
}
end += len;
byte[] part = new byte[4 + len];
part[0] = (byte)0; // proto
part[1] = (byte)msgID;
part[2] = (byte)ii;
part[3] = (byte)count;
System.arraycopy( msg, start, part, 4, len );
result[ii] = part;
start = end;
}
return result;
}
private void receive( SMS_CMD cmd, byte[] data, String phone )
{
DataInputStream dis =
@ -473,6 +536,19 @@ public class SMSService extends XWService {
}
}
private void receiveBuffer( byte[] buffer, String senderPhone )
{
byte proto = buffer[0];
int id = buffer[1];
int index = buffer[2];
int count = buffer[3];
byte[] rest = new byte[buffer.length - 4];
System.arraycopy( buffer, 4, rest, 0, rest.length );
tryAssemble( senderPhone, id, index, count, rest );
sendResult( MultiEvent.SMS_RECEIVE_OK );
}
private void receiveBuffer( String as64, String senderPhone )
{
String[] parts = as64.split( ":" );
@ -488,6 +564,34 @@ public class SMSService extends XWService {
}
}
private void tryAssemble( String senderPhone, int id, int index,
int count, byte[] msg )
{
if ( index == 0 && count == 1 ) {
disAssemble( senderPhone, msg );
} else {
// required? Should always be in main thread.
synchronized( s_partialMsgs ) {
HashMap<Integer, MsgStore> perPhone =
s_partialMsgs.get( senderPhone );
if ( null == perPhone ) {
perPhone = new HashMap <Integer, MsgStore>();
s_partialMsgs.put( senderPhone, perPhone );
}
MsgStore store = perPhone.get( id );
if ( null == store ) {
store = new MsgStore( id, count, false );
perPhone.put( id, store );
}
if ( store.add( index, msg ).isComplete() ) {
disAssemble( senderPhone, store.messageData() );
perPhone.remove( id );
}
}
}
}
private void tryAssemble( String senderPhone, int id, int index,
int count, String msg )
{
@ -504,18 +608,41 @@ public class SMSService extends XWService {
}
MsgStore store = perPhone.get( id );
if ( null == store ) {
store = new MsgStore( id, count );
store = new MsgStore( id, count, true );
perPhone.put( id, store );
}
if ( store.add( index, msg ).isComplete() ) {
disAssemble( senderPhone, store.message() );
disAssemble( senderPhone, store.messageText() );
perPhone.remove( id );
}
}
}
}
private void disAssemble( String senderPhone, byte[] fullMsg )
{
DataInputStream dis =
new DataInputStream( new ByteArrayInputStream(fullMsg) );
try {
byte proto = dis.readByte();
if ( SMS_PROTO_VERSION != proto ) {
DbgUtils.logf( "SMSService.disAssemble: bad proto %d; dropping",
proto );
} else {
SMS_CMD cmd = SMS_CMD.values()[dis.readByte()];
byte[] rest = new byte[dis.available()];
dis.read( rest );
receive( cmd, rest, senderPhone );
}
} catch ( java.io.IOException ioe ) {
DbgUtils.loge( ioe );
} catch ( ArrayIndexOutOfBoundsException oob ) {
// enum this older code doesn't know about; drop it
DbgUtils.logf( "disAssemble: dropping message with too-new enum" );
}
}
private void disAssemble( String senderPhone, String fullMsg )
{
byte[] data = XwJNI.base64Decode( fullMsg );
@ -593,6 +720,39 @@ public class SMSService extends XWService {
return success;
}
private boolean sendBuffers( byte[][] fragments, String phone, byte[] data )
{
boolean success = false;
try {
SmsManager mgr = SmsManager.getDefault();
PendingIntent sent = makeStatusIntent( MSG_SENT );
PendingIntent delivery = makeStatusIntent( MSG_DELIVERED );
for ( byte[] fragment : fragments ) {
mgr.sendDataMessage( phone, null, (short)3344, fragment,
sent, delivery );
}
if ( s_showToasts ) {
DbgUtils.showf( this, "sent %dth msg", s_nSent );
}
success = true;
} catch ( IllegalArgumentException iae ) {
DbgUtils.logf( "sendBuffers(%s): %s", phone, iae.toString() );
} catch ( NullPointerException npe ) {
DbgUtils.showf( this, "Switching to regular SMS" );
s_asData = new Boolean( false );
XWPrefs.setPrefsBoolean( this, R.string.key_send_data_sms,
false );
success = sendAsText( data, phone );
} catch ( Exception ee ) {
DbgUtils.loge( ee );
}
ConnStatusHandler.updateStatusOut( this, null,
CommsConnType.COMMS_CONN_SMS,
success );
return success;
}
private static void fillInviteIntent( Intent intent, String phone,
int gameID, String gameName,
int lang, String dict,
@ -691,14 +851,39 @@ public class SMSService extends XWService {
@Override
public void onReceive(Context arg0, Intent arg1)
{
if ( Activity.RESULT_OK == getResultCode() ) {
DbgUtils.logf( "SUCCESS!!!" );
} else {
DbgUtils.logf( "FAILURE!!!" );
}
DbgUtils.logf( "SMS delivery result: %s",
Activity.RESULT_OK == getResultCode()
? "SUCCESS" : "FAILURE" );
}
};
registerReceiver( m_receiveReceiver, new IntentFilter(MSG_DELIVERED) );
m_prefsListener = new OnSharedPreferenceChangeListener() {
public void onSharedPreferenceChanged( SharedPreferences sp,
String key ) {
if ( key.equals( getString( R.string.key_show_sms ) ) ) {
s_showToasts = null;
} else if ( key.equals( getString( R.string
.key_send_data_sms ))) {
s_asData = null;
}
}
};
SharedPreferences sp
= PreferenceManager.getDefaultSharedPreferences( this );
sp.registerOnSharedPreferenceChangeListener( m_prefsListener );
}
private boolean sendAsText( byte[] data, String phone )
{
boolean success = false;
try {
String[] msgs = breakAndEncode( XwJNI.base64Encode( data ) );
success = sendBuffers( msgs, phone );
} catch ( java.io.IOException ioe ) {
DbgUtils.loge( ioe );
}
return success;
}
private class SMSMsgSink extends MultiMsgSink {
@ -729,41 +914,68 @@ public class SMSService extends XWService {
}
private class MsgStore {
String[] m_msgs;
String[] m_msgsText;
byte[][] m_msgsData;
int m_msgID;
int m_haveCount;
int m_fullLength;
public MsgStore( int id, int count )
public MsgStore( int id, int count, boolean usingStrings )
{
m_msgID = id;
m_msgs = new String[count];
if ( usingStrings ) {
m_msgsText = new String[count];
} else {
m_msgsData = new byte[count][];
}
m_fullLength = 0;
}
public MsgStore add( int index, String msg )
{
if ( null == m_msgs[index] ) {
if ( null == m_msgsText[index] ) {
++m_haveCount;
m_fullLength += msg.length();
}
m_msgs[index] = msg;
m_msgsText[index] = msg;
return this;
}
public MsgStore add( int index, byte[] msg )
{
if ( null == m_msgsData[index] ) {
++m_haveCount;
m_fullLength += msg.length;
}
m_msgsData[index] = msg;
return this;
}
public boolean isComplete()
{
boolean complete = m_msgs.length == m_haveCount;
int count = null != m_msgsText ? m_msgsText.length : m_msgsData.length;
boolean complete = count == m_haveCount;
return complete;
}
public String message()
public String messageText()
{
StringBuffer sb = new StringBuffer(m_fullLength);
for ( String msg : m_msgs ) {
for ( String msg : m_msgsText ) {
sb.append( msg );
}
return sb.toString();
}
public byte[] messageData()
{
byte[] result = new byte[m_fullLength];
int indx = 0;
for ( byte[] msg : m_msgsData ) {
System.arraycopy( msg, 0, result, indx, msg.length );
indx += msg.length;
}
return result;
}
}
}

View file

@ -75,7 +75,7 @@ public class StudyListActivity extends ListActivity {
@Override
protected Dialog onCreateDialog( int id )
{
Dialog dialog = m_dlgt.createDialog( id );
Dialog dialog = m_dlgt.onCreateDialog( id );
if ( null == dialog ) {
dialog = super.onCreateDialog( id );
}

View file

@ -74,6 +74,7 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
private static final String k_PARAMS = "params";
private static final String k_DEVID = "did";
private static final String k_XLATEINFO = "xlatinfo";
private static final String k_APPGITREV = "apprev";
@Override
public void onReceive( Context context, Intent intent )
@ -122,6 +123,13 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
JSONObject params = new JSONObject();
PackageManager pm = context.getPackageManager();
String packageName = context.getPackageName();
int versionCode;
try {
versionCode = pm.getPackageInfo( packageName, 0 ).versionCode;
} catch ( PackageManager.NameNotFoundException nnfe ) {
DbgUtils.loge( nnfe );
versionCode = 0;
}
// App update
if ( Utils.isGooglePlayApp( context ) ) {
@ -130,8 +138,6 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
String installer = pm.getInstallerPackageName( packageName );
try {
int versionCode = pm.getPackageInfo( packageName, 0 ).versionCode;
JSONObject appParams = new JSONObject();
appParams.put( k_NAME, packageName );
@ -143,8 +149,6 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
}
params.put( k_APP, appParams );
params.put( k_DEVID, XWPrefs.getDevID( context ) );
} catch ( PackageManager.NameNotFoundException nnfe ) {
DbgUtils.loge( nnfe );
} catch ( org.json.JSONException jse ) {
DbgUtils.loge( jse );
}
@ -166,7 +170,7 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
}
// Xlations update
JSONObject xlationUpdate = LocUtils.makeForXlationUpdate( context );
JSONArray xlationUpdate = LocUtils.makeForXlationUpdate( context );
if ( null != xlationUpdate ) {
try {
params.put( k_XLATEINFO, xlationUpdate );
@ -176,8 +180,16 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
}
if ( 0 < params.length() ) {
new UpdateQueryTask( context, params, fromUI, pm,
packageName, dals ).execute();
try {
params.put( k_APPGITREV, BuildConstants.GIT_HASH );
params.put( k_NAME, packageName );
params.put( k_AVERS, versionCode );
DbgUtils.logf( "current update: %s", params.toString() );
new UpdateQueryTask( context, params, fromUI, pm,
packageName, dals ).execute();
} catch ( org.json.JSONException jse ) {
DbgUtils.loge( jse );
}
}
}
@ -387,8 +399,8 @@ public class UpdateCheckReceiver extends BroadcastReceiver {
// translations info
if ( jobj.has( k_XLATEINFO ) ) {
JSONObject data = jobj.getJSONObject( k_XLATEINFO );
int nAdded = LocUtils.addXlation( m_context, data );
JSONArray data = jobj.getJSONArray( k_XLATEINFO );
int nAdded = LocUtils.addXlations( m_context, data );
if ( 0 < nAdded ) {
gotOne = true;
String msg = LocUtils.getString( m_context, R.string

View file

@ -97,19 +97,39 @@ public class Utils {
return s_isFirstBootThisVersion;
}
// Does the device have ability to send SMS -- e.g. is it a phone
// and not a Kindle Fire. Not related to XWApp.SMSSUPPORTED
public static boolean isGSMPhone( Context context )
{
boolean result = false;
TelephonyManager tm = (TelephonyManager)
context.getSystemService( Context.TELEPHONY_SERVICE );
if ( null != tm ) {
result = TelephonyManager.PHONE_TYPE_GSM == tm.getPhoneType();
}
return result;
}
// Does the device have ability to send SMS -- e.g. is it a phone and not
// a Kindle Fire. Not related to XWApp.SMSSUPPORTED. Note that as a
// temporary workaround for KitKat having broken use of non-data messages,
// we only support SMS on kitkat if data messages have been turned on (and
// that's not allowed except on GSM phones.)
public static boolean deviceSupportsSMS( Context context )
{
if ( null == s_deviceSupportSMS ) {
boolean doesSMS = false;
// TEMPORARY: disable SMS on KITKAT
if ( 19 > Integer.valueOf( android.os.Build.VERSION.SDK ) ) {
// TEMPORARY: disable SMS on KITKAT UNLESS use-text turned on
boolean preKitkat = 19 > Integer.valueOf( android.os.Build.VERSION.SDK);
boolean usingData =
XWPrefs.getPrefsBoolean( context, R.string.key_send_data_sms,
false );
if ( preKitkat || usingData ) {
TelephonyManager tm = (TelephonyManager)
context.getSystemService(Context.TELEPHONY_SERVICE);
if ( null != tm ) {
int type = tm.getPhoneType();
doesSMS = TelephonyManager.PHONE_TYPE_NONE != type;
doesSMS = (usingData && !preKitkat)
? TelephonyManager.PHONE_TYPE_GSM == type
: TelephonyManager.PHONE_TYPE_NONE != type;
}
}
s_deviceSupportSMS = new Boolean( doesSMS );

View file

@ -430,7 +430,7 @@ public class XWPrefs {
setPrefsString( context, keyID, TextUtils.join( "\n", value ) );
}
public static String getLocale( Context context )
public static String getFakeLocale( Context context )
{
return getPrefsString( context, R.string.key_xlations_locale );
}

View file

@ -36,12 +36,18 @@ import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.TextView;
import android.preference.Preference;
import android.preference.PreferenceGroup;
import android.preference.PreferenceActivity;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONObject;
@ -55,12 +61,8 @@ import org.eehouse.android.xw4.Utils;
import org.eehouse.android.xw4.XWPrefs;
public class LocUtils {
// Keep this in sync with gen_loc_ids.py and what's used in the menu.xml
// files to mark me-localized strings.
private static final int FMT_LEN = 4;
private static final String k_LOCALE = "locale";
private static final String k_XLATPROTO = "proto";
private static final int XLATE_CUR_VERSION = 1;
private static final String k_XLATEVERS = "xlatevers";
private static Map<String, String> s_xlationsLocal = null;
@ -121,6 +123,11 @@ public class LocUtils {
xlateView( activity, Utils.getContentView( activity ) );
}
public static void xlatePreferences( PreferenceActivity activity )
{
xlatePreferences( activity, activity.getPreferenceScreen(), 0 );
}
public static void xlateView( Context context, View view )
{
DbgUtils.logf( "xlateView() top level" );
@ -132,6 +139,15 @@ public class LocUtils {
xlateMenu( activity, menu, 0 );
}
private static String xlateString( Context context, CharSequence str )
{
String result = null;
if ( null != str ) {
result = xlateString( context, str.toString() );
}
return result;
}
public static String xlateString( Context context, String str )
{
if ( LocIDs.getS_MAP( context ).containsKey( str ) ) {
@ -225,55 +241,77 @@ public class LocUtils {
public static void saveLocalData( Context context )
{
DBUtils.saveXlations( context, s_curLocale, s_xlationsLocal, false );
DBUtils.saveXlations( context, getCurLocale( context ),
s_xlationsLocal, false );
}
public static JSONObject makeForXlationUpdate( Context context )
public static JSONArray makeForXlationUpdate( Context context )
{
JSONObject result = null;
if ( null != s_curLocale && 0 < s_curLocale.length() ) {
try {
String version = DBUtils.getStringFor( context, k_XLATEVERS, "0" );
result = new JSONObject()
.put( k_XLATPROTO, XLATE_CUR_VERSION )
.put( k_LOCALE, s_curLocale )
.put( k_XLATEVERS, version );
} catch ( org.json.JSONException jse ) {
DbgUtils.loge( jse );
}
String locale = getCurLocale( context );
String fake = XWPrefs.getFakeLocale( context );
JSONArray result = new JSONArray()
.put( entryForLocale( context, locale ) );
if ( null != fake && 0 < fake.length() && ! fake.equals(locale) ) {
result.put( entryForLocale( context, fake ) );
}
return result;
}
private static JSONObject entryForLocale( Context context, String locale )
{
JSONObject result = null;
try {
String version =
DBUtils.getStringFor( context, localeKey(locale), "0" );
result = new JSONObject()
.put( k_LOCALE, locale )
.put( k_XLATEVERS, version );
} catch ( org.json.JSONException jse ) {
DbgUtils.loge( jse );
}
return result;
}
private static String localeKey( String locale )
{
return String.format( "%s:%s", k_XLATEVERS, locale );
}
private static final String k_OLD = "old";
private static final String k_NEW = "new";
private static final String k_PAIRS = "pairs";
public static int addXlation( Context context, JSONObject data )
public static int addXlations( Context context, JSONArray data )
{
int nAdded = 0;
try {
int newVersion = data.getInt( k_NEW );
JSONArray pairs = data.getJSONArray( k_PAIRS );
DbgUtils.logf( "got pairs of len %d, version %d", pairs.length(),
newVersion );
int nLocales = data.length();
for ( int ii = 0; ii < nLocales; ++ii ) {
JSONObject entry = data.getJSONObject( ii );
String locale = entry.getString( k_LOCALE );
String newVersion = entry.getString( k_NEW );
JSONArray pairs = entry.getJSONArray( k_PAIRS );
DbgUtils.logf( "addXlations: locale %s: got pairs of len %d, version %s", locale,
pairs.length(), newVersion );
int len = pairs.length();
Map<String,String> newXlations = new HashMap<String,String>( len );
for ( int ii = 0; ii < len; ++ii ) {
JSONObject pair = pairs.getJSONObject( ii );
String key = pair.getString( "en" );
String txt = pair.getString( "loc" );
newXlations.put( key, txt );
int len = pairs.length();
Map<String,String> newXlations = new HashMap<String,String>( len );
for ( int jj = 0; jj < len; ++jj ) {
JSONObject pair = pairs.getJSONObject( jj );
int id = pair.getInt( "id" );
String key = context.getString( id );
Assert.assertNotNull( key );
String txt = pair.getString( "loc" );
txt = replaceEscaped( txt );
newXlations.put( key, txt );
}
DBUtils.saveXlations( context, locale, newXlations, true );
DBUtils.setStringFor( context, localeKey(locale), newVersion );
nAdded += len;
}
DBUtils.saveXlations( context, s_curLocale, newXlations, true );
DBUtils.setStringFor( context, k_XLATEVERS,
String.format( "%d", newVersion ) );
s_xlationsBlessed = null;
loadXlations( context );
nAdded = len;
} catch ( org.json.JSONException jse ) {
DbgUtils.loge( jse );
}
@ -334,13 +372,23 @@ public class LocUtils {
}
}
private static void loadXlations( Context context )
private static String getCurLocale( Context context )
{
if ( null == s_curLocale ) {
s_curLocale = XWPrefs.getLocale( context );
String locale = XWPrefs.getFakeLocale( context );
if ( null == locale || 0 == locale.length() ) {
locale = Locale.getDefault().toString();
}
s_curLocale = locale;
}
return s_curLocale;
}
private static void loadXlations( Context context )
{
if ( null == s_xlationsLocal || null == s_xlationsBlessed ) {
Object[] asObjs = DBUtils.getXlations( context, s_curLocale );
Object[] asObjs = DBUtils.getXlations( context,
getCurLocale( context ) );
s_xlationsLocal = (Map<String,String>)asObjs[0];
s_xlationsBlessed = (Map<String,String>)asObjs[1];
DbgUtils.logf( "loadXlations: got %d local strings, %d blessed strings",
@ -367,16 +415,6 @@ public class LocUtils {
return s_idsToKeys.get( id );
}
private static boolean isEnabled( Context context )
{
if ( null == s_enabled ) {
s_curLocale = XWPrefs.getLocale( context );
s_enabled = new Boolean( null != s_curLocale &&
0 < s_curLocale.length() );
}
return s_enabled;
}
private static void xlateView( Context context, View view, int depth )
{
// DbgUtils.logf( "xlateView(depth=%d, view=%s, canRecurse=%b)", depth,
@ -388,11 +426,6 @@ public class LocUtils {
} else if ( view instanceof TextView ) {
TextView tv = (TextView)view;
tv.setText( xlateString( context, tv.getText().toString() ) );
// } else if ( view instanceof CheckBox ) {
// CheckBox box = (CheckBox)view;
// String str = box.getText().toString();
// str = xlateString( context, str );
// box.setText( str );
} else if ( view instanceof Spinner ) {
Spinner sp = (Spinner)view;
CharSequence prompt = sp.getPrompt();
@ -415,23 +448,80 @@ public class LocUtils {
}
}
public static void xlatePreferences( Context context, Preference pref,
int depth )
{
// DbgUtils.logf( "xlatePreferences(depth=%d, view=%s, canRecurse=%b)", depth,
// pref.getClass().getName(), pref instanceof PreferenceGroup );
String str = xlateString( context, pref.getSummary() );
if ( null != str ) {
pref.setSummary( str );
}
str = xlateString( context, pref.getTitle() );
if ( null != str ) {
pref.setTitle( str );
}
if ( pref instanceof PreferenceGroup ) {
PreferenceGroup group = (PreferenceGroup)pref;
int count = group.getPreferenceCount();
for ( int ii = 0; ii < count; ++ii ) {
xlatePreferences( context, group.getPreference(ii), 1 + depth );
}
}
}
// This is for testing, but the ability to pull the formatters will be
// critical for validating local transations of strings containing
// formatters.
private static String toUpperCase( String str )
{
String[] parts = str.split( "%[\\d]\\$[ds]" );
StringBuilder sb = new StringBuilder();
int offset = 0;
for ( String part : parts ) {
sb.append( part.toUpperCase() );
offset += part.length();
if ( offset < str.length() ) {
sb.append( str.substring( offset, offset + FMT_LEN ) );
offset += FMT_LEN;
String result = null;
if ( UPPER_CASE ) {
String[] parts = str.split( "%[\\d]\\$[ds]" );
StringBuilder sb = new StringBuilder();
int offset = 0;
for ( String part : parts ) {
sb.append( part.toUpperCase() );
offset += part.length();
if ( offset < str.length() ) {
sb.append( str.substring( offset, offset + FMT_LEN ) );
offset += FMT_LEN;
}
}
result = sb.toString();
}
return sb.toString();
return result;
}
private static Pattern s_patUnicode = Pattern.compile("(\\\\[Uu][0-9a-fA-F]{4})");
private static Pattern s_patCr = Pattern.compile("\\\\n");
private static String replaceEscaped( String txt )
{
// String orig = txt;
// Swap unicode escapes for real chars
Matcher matcher = s_patUnicode.matcher( txt );
StringBuffer sb = new StringBuffer();
while ( matcher.find() ) {
int start = matcher.start();
int end = matcher.end();
String match = txt.substring( start, end );
char ch = (char)Integer.parseInt( match.substring(2), 16 );
matcher.appendReplacement( sb, String.valueOf(ch) );
}
matcher.appendTail(sb);
txt = sb.toString();
// Swap in real carriage returns
txt = s_patCr.matcher( txt ).replaceAll( "\n" );
// if ( ! orig.equals( txt ) ) {
// DbgUtils.logf( "replaceEscaped: <<%s>> -> <<%s>>", orig, txt );
// }
return txt;
}
public static AlertDialog.Builder makeAlertBuilder( Context context )

View file

@ -77,7 +77,7 @@ for PACK_UNSIGNED in $FILES; do
if [ -n "$XW_RELEASE_SCP_DEST" ]; then
echo "running scp ${TARGET} $XW_RELEASE_SCP_DEST"
scp "${TARGET}" "$XW_RELEASE_SCP_DEST"
scp "${TARGET}" "$XW_RELEASE_SCP_DEST" || true
else
echo "XW_RELEASE_SCP_DEST not set; you're on your own"
fi

View file

@ -2,6 +2,13 @@
set -e -u
usage() {
echo "usage: $0 <variant> <relay_vers> <chatSupported> <thumbSupported>"
exit 1
}
[ $# -eq 4 ] || usage
VARIANT=$1
CLIENT_VERS_RELAY=$2
CHAT_SUPPORTED=$3
@ -12,6 +19,11 @@ cd $(dirname $0)
cd ../
GITVERSION=$(../scripts/gitversion.sh)
if git rev-parse $GITVERSION 2>/dev/null 1>/dev/null; then
GIT_HASH=$(git rev-parse $GITVERSION)
else
GIT_HASH=unknown
fi
# TODO: deal with case where there's no hash available -- exported
# code maybe? Better: gitversion.sh does that.
@ -34,6 +46,7 @@ cat <<EOF > ${BUILD_DIR}/src/org/eehouse/android/${VARIANT}/BuildConstants.java
package org.eehouse.android.${VARIANT};
class BuildConstants {
public static final String GIT_REV = "$SHORTVERS";
public static final String GIT_HASH = "$GIT_HASH";
public static final short CLIENT_VERS_RELAY = $CLIENT_VERS_RELAY;
public static final boolean CHAT_SUPPORTED = $CHAT_SUPPORTED;
public static final boolean THUMBNAIL_SUPPORTED = $THUMBNAIL_SUPPORTED;

View file

@ -38,6 +38,7 @@ SRC = \
udpager.cpp \
udpqueue.cpp \
xwrelay.cpp \
querybld.cpp \
# STATIC ?= -static
GITINFO = gitversion.txt

View file

@ -43,11 +43,8 @@
static DBMgr* s_instance = NULL;
#define DELIM "\1"
#define MAX_NUM_PLAYERS 4
static void formatParams( char* paramValues[], int nParams, const char* fmt,
char* buf, int bufLen, ... );
static int here_less_seed( const char* seeds, int perDeviceSum,
unsigned short seed );
static void destr_function( void* conn );
@ -105,20 +102,21 @@ DBMgr::AddNew( const char* cookie, const char* connName, CookieID cid,
if ( !cookie ) cookie = "";
if ( !connName ) connName = "";
const char* command = "INSERT INTO " GAMES_TABLE
" (cid, room, connName, nTotal, lang, pub)"
" VALUES( $1, $2, $3, $4, $5, $6 )";
int nParams = 6;
char* paramValues[nParams];
char buf[512];
formatParams( paramValues, nParams,
"%d"DELIM"%s"DELIM"%s"DELIM"%d"DELIM"%d"DELIM"%s",
buf, sizeof(buf), cid, cookie, connName, nPlayersT,
langCode, isPublic?"TRUE":"FALSE" );
QueryBuilder qb;
qb.appendQueryf( "INSERT INTO " GAMES_TABLE
" (cid, room, connName, nTotal, lang, pub)"
" VALUES( $$, $$, $$, $$, $$, $$ )" )
.appendParam(cid)
.appendParam(cookie)
.appendParam(connName)
.appendParam(nPlayersT)
.appendParam(langCode)
.appendParam(isPublic?"TRUE":"FALSE" )
.finish();
PGresult* result = PQexecParams( getThreadConn(), command,
nParams, NULL,
paramValues,
PGresult* result = PQexecParams( getThreadConn(), qb.c_str(),
qb.paramCount(), NULL,
qb.paramValues(),
NULL, NULL, 0 );
if ( PGRES_COMMAND_OK != PQresultStatus(result) ) {
logf( XW_LOGERROR, "PQexec=>%s;%s", PQresStatus(PQresultStatus(result)),
@ -271,28 +269,27 @@ DBMgr::SeenSeed( const char* cookie, unsigned short seed,
char* connNameBuf, int bufLen, int* nPlayersHP,
CookieID* cid )
{
int nParams = 5;
char* paramValues[nParams];
char buf[512];
formatParams( paramValues, nParams,
"%s"DELIM"%d"DELIM"%d"DELIM"%d"DELIM"%s", buf, sizeof(buf),
cookie, langCode, nPlayersT, seed,
wantsPublic?"TRUE":"FALSE" );
QueryBuilder qb;
qb.appendQueryf( "SELECT cid, connName, seeds, sum_array(nPerDevice) FROM "
GAMES_TABLE
" WHERE NOT dead"
" AND room ILIKE $$"
" AND lang = $$"
" AND nTotal = $$"
" AND $$ = ANY(seeds)"
" AND $$ = pub"
" ORDER BY ctime DESC"
" LIMIT 1")
.appendParam(cookie)
.appendParam(langCode)
.appendParam(nPlayersT)
.appendParam(seed)
.appendParam(wantsPublic?"TRUE":"FALSE" )
.finish();
const char* cmd = "SELECT cid, connName, seeds, sum_array(nPerDevice) FROM "
GAMES_TABLE
" WHERE NOT dead"
" AND room ILIKE $1"
" AND lang = $2"
" AND nTotal = $3"
" AND $4 = ANY(seeds)"
" AND $5 = pub"
" ORDER BY ctime DESC"
" LIMIT 1";
PGresult* result = PQexecParams( getThreadConn(), cmd,
nParams, NULL,
paramValues,
PGresult* result = PQexecParams( getThreadConn(), qb.c_str(),
qb.paramCount(), NULL,
qb.paramValues(),
NULL, NULL, 0 );
bool found = 1 == PQntuples( result );
if ( found ) {
@ -312,31 +309,28 @@ DBMgr::FindOpen( const char* cookie, int lang, int nPlayersT, int nPlayersH,
bool wantsPublic, char* connNameBuf, int bufLen,
int* nPlayersHP )
{
CookieID cid = 0;
QueryBuilder qb;
qb.appendQueryf("SELECT cid, connName, sum_array(nPerDevice) FROM "
GAMES_TABLE
" WHERE NOT dead"
" AND room ILIKE $$"
" AND lang = $$"
" AND nTotal = $$"
" AND $$ <= nTotal-sum_array(nPerDevice)"
" AND $$ = pub"
" LIMIT 1")
.appendParam(cookie)
.appendParam(lang)
.appendParam(nPlayersT)
.appendParam(nPlayersH)
.appendParam(wantsPublic?"TRUE":"FALSE" )
.finish();
int nParams = 5;
char* paramValues[nParams];
char buf[512];
formatParams( paramValues, nParams,
"%s"DELIM"%d"DELIM"%d"DELIM"%d"DELIM"%s", buf, sizeof(buf),
cookie, lang, nPlayersT, nPlayersH, wantsPublic?"TRUE":"FALSE" );
/* NOTE: ILIKE, for case-insensitive comparison, is a postgres extension
to SQL. */
const char* cmd = "SELECT cid, connName, sum_array(nPerDevice) FROM "
GAMES_TABLE
" WHERE NOT dead"
" AND room ILIKE $1"
" AND lang = $2"
" AND nTotal = $3"
" AND $4 <= nTotal-sum_array(nPerDevice)"
" AND $5 = pub"
" LIMIT 1";
PGresult* result = PQexecParams( getThreadConn(), cmd,
nParams, NULL,
paramValues,
PGresult* result = PQexecParams( getThreadConn(), qb.c_str(),
qb.paramCount(), NULL,
qb.paramValues(),
NULL, NULL, 0 );
CookieID cid = 0;
if ( 1 == PQntuples( result ) ) {
cid = atoi( PQgetvalue( result, 0, 0 ) );
snprintf( connNameBuf, bufLen, "%s", PQgetvalue( result, 0, 1 ) );
@ -396,14 +390,21 @@ DBMgr::RegisterDevice( const DevID* host, int clientVersion,
devID = (DevIDRelay)random();
} while ( DEVID_NONE == devID );
StrWPF query;
query.catf( "INSERT INTO " DEVICES_TABLE " (id, devTypes[1],"
" devids[1], clntVers, versdesc, model, osvers)"
" VALUES( %d, %d, '%s', %d, '%s', '%s', '%s' )",
devID, host->m_devIDType, devidStr, clientVersion,
desc, model, osVers );
logf( XW_LOGINFO, "%s: %s", __func__, query.c_str() );
success = execSql( query );
QueryBuilder qb;
qb.appendQueryf( "INSERT INTO " DEVICES_TABLE " (id, devTypes[1],"
" devids[1], clntVers, versdesc, model, osvers)"
" VALUES($$, $$, $$, $$, $$, $$, $$)" );
qb.appendParam( devID )
.appendParam( host->m_devIDType )
.appendParam( devidStr )
.appendParam( clientVersion )
.appendParam( desc )
.appendParam( model )
.appendParam( osVers )
.finish();
success = execParams( qb );
}
}
return devID;
@ -420,15 +421,17 @@ DBMgr::ReregisterDevice( DevIDRelay relayID, const DevID* host,
const char* const desc, int clientVersion,
const char* const model, const char* const osVers )
{
// First update the existing
StrWPF query;
query.catf( "UPDATE " DEVICES_TABLE " SET "
"devTypes = array_prepend( %d, devTypes), "
"devids = array_prepend('%s', devids), ",
host->m_devIDType, host->m_devIDString.c_str() );
QueryBuilder qb;
qb.appendQueryf( "UPDATE " DEVICES_TABLE " SET "
"devTypes = array_prepend($$, devTypes), "
"devids = array_prepend($$, devids), " )
formatUpdate( query, true, desc, clientVersion, model, osVers, relayID );
execSql( query );
.appendParam( host->m_devIDType )
.appendParam( host->m_devIDString.c_str() );
formatUpdate( qb, true, desc, clientVersion, model, osVers, relayID );
qb.finish();
execParams( qb );
}
// Return true if the relayID exists in the DB already
@ -445,10 +448,11 @@ DBMgr::UpdateDevice( DevIDRelay relayID, const char* const desc,
}
if ( exists ) {
StrWPF query;
query.catf( "UPDATE " DEVICES_TABLE " SET " );
formatUpdate( query, false, desc, clientVersion, model, osVers, relayID );
execSql( query );
QueryBuilder qb;
qb.appendQueryf( "UPDATE " DEVICES_TABLE " SET " );
formatUpdate( qb, false, desc, clientVersion, model, osVers, relayID );
qb.finish();
execParams( qb );
}
return exists;
}
@ -460,26 +464,33 @@ DBMgr::UpdateDevice( DevIDRelay relayID )
}
void
DBMgr::formatUpdate( StrWPF& query, bool append, const char* const desc,
DBMgr::formatUpdate( QueryBuilder& qb,
bool append, const char* const desc,
int clientVersion, const char* const model,
const char* const osVers, DevIDRelay relayID )
{
if ( append ) {
query.catf( "mtimes=array_prepend('now', mtimes)" ); // FIXME: too many
qb.appendQueryf( "mtimes=array_prepend('now', mtimes)" ); // FIXME: too many
} else {
query.catf( "mtimes[1]='now'" );
qb.appendQueryf( "mtimes[1]='now'" );
}
if ( NULL != desc && '\0' != desc[0] ) {
query.catf( ", clntVers=%d, versDesc='%s'", clientVersion, desc );
qb.appendQueryf( ", clntVers=$$" )
.appendParam( clientVersion )
.appendQueryf( ", versDesc=$$" )
.appendParam( desc );
}
if ( NULL != model && '\0' != model[0] ) {
query.catf( ", model='%s'", model );
qb.appendQueryf( ", model=$$" )
.appendParam( model );
}
if ( NULL != osVers && '\0' != osVers[0] ) {
query.catf( ", osvers='%s'", osVers );
qb.appendQueryf( ", osvers=$$" )
.appendParam( osVers );
}
query.catf( " WHERE id = %d", relayID );
qb.appendQueryf( " WHERE id = $$" )
.appendParam( relayID );
}
HostID
@ -831,6 +842,22 @@ DBMgr::execSql( const char* const query )
return ok;
}
bool
DBMgr::execParams( QueryBuilder& qb )
{
PGresult* result = PQexecParams( getThreadConn(), qb.c_str(),
qb.paramCount(), NULL,
qb.paramValues(),
NULL, NULL, 0 );
bool success = PGRES_COMMAND_OK == PQresultStatus( result );
if ( !success ) {
logf( XW_LOGERROR, "PQexecParams(%s)=>%s;%s", qb.c_str(),
PQresStatus(PQresultStatus(result)),
PQresultErrorMessage(result) );
}
return success;
}
void
DBMgr::readArray( const char* const connName, const char* column, int arr[] ) /* len 4 */
{
@ -1255,31 +1282,6 @@ void DBMgr::clearHasNoMessages( DevIDRelay devid )
assert( !hasNoMessages( devid ) );
}
static void
formatParams( char* paramValues[], int nParams, const char* fmt, char* buf,
int bufLen, ... )
{
va_list ap;
va_start( ap, bufLen );
int len = vsnprintf( buf, bufLen, fmt, ap );
assert( buf[len] == '\0' );
int pnum;
char* ptr = buf;
for ( pnum = 0; pnum < nParams; ++pnum ) {
paramValues[pnum] = ptr;
for ( ; *ptr != '\0' && *ptr != DELIM[0]; ++ptr ) {
// do nothing
assert( ptr < &buf[bufLen] );
}
// we've found an end
*ptr = '\0';
++ptr;
}
va_end(ap);
}
static int
here_less_seed( const char* seeds, int sumPerDevice, unsigned short seed )
{

View file

@ -32,6 +32,7 @@
#include "xwrelay_priv.h"
#include "devid.h"
#include "strwpf.h"
#include "querybld.h"
using namespace std;
@ -149,6 +150,7 @@ class DBMgr {
DBMgr();
bool execSql( const string& query );
bool execSql( const char* const query ); /* no-results query */
bool execParams( QueryBuilder& qb );
void readArray( const char* const connName, const char* column, int arr[] );
DevIDRelay getDevID( const char* connName, int hid );
DevIDRelay getDevID( const DevID* devID );
@ -160,7 +162,7 @@ class DBMgr {
bool nullConnnameOK );
int CountStoredMessages( const char* const connName, int hid );
bool UpdateDevice( DevIDRelay relayID );
void formatUpdate( StrWPF& query, bool append, const char* const desc,
void formatUpdate( QueryBuilder& qb, bool append, const char* const desc,
int clientVersion, const char* const model,
const char* const osVers, DevIDRelay relayID );

View file

@ -0,0 +1,78 @@
/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/*
* Copyright 2014 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.
*
* 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.
*/
#include <stdarg.h>
#include "querybld.h"
#include "xwrelay_priv.h"
QueryBuilder&
QueryBuilder::appendQueryf( const char* fmt, ... )
{
bool done;
do {
va_list ap;
va_start( ap, fmt );
done = m_query.catf( fmt, ap );
va_end( ap );
} while ( !done );
return *this;
}
QueryBuilder&
QueryBuilder::appendParam( const char* value )
{
m_paramIndices.push_back( m_paramBuf.size() );
m_paramBuf.catf( "%s%c", value, '\0' );
return *this;
}
QueryBuilder&
QueryBuilder::appendParam( int value )
{
m_paramIndices.push_back( m_paramBuf.size() );
m_paramBuf.catf( "%d%c", value, '\0' );
return *this;
}
/* When done adding params, some of which contain $$, turn these into an order
* progression of $1, $2 .. $9. Note assumption that we don't go above 9 since
*/
void
QueryBuilder::finish()
{
assert( 0 == m_paramValues.size() );
size_t ii;
const char* base = m_paramBuf.c_str();
for ( ii = 0; ii < m_paramIndices.size(); ++ii ) {
const char* ptr = m_paramIndices[ii] + base;
m_paramValues.push_back( ptr );
}
for ( size_t count = 0; ; ++count ) {
const char* str = m_query.c_str();
const char* ptr = strstr( str, "$$" );
if ( !ptr ) {
assert( count == m_paramIndices.size() );
break;
}
assert( count < 9 );
m_query[1 + ptr - str] = '1' + count;
}
}

47
xwords4/relay/querybld.h Normal file
View file

@ -0,0 +1,47 @@
/* -*-mode: C; fill-column: 78; c-basic-offset: 4; -*- */
/*
* Copyright 2014 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.
*
* 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.
*/
#ifndef _QUERYBLD_H_
#define _QUERYBLD_H_
#include <vector>
#include "strwpf.h"
using namespace std;
class QueryBuilder {
public:
QueryBuilder& appendQueryf( const char* fmt, ... );
QueryBuilder& appendParam( const char* value );
QueryBuilder& appendParam( int value );
void finish();
int paramCount() const { return m_paramValues.size(); }
const char* const* paramValues() const { return &m_paramValues[0]; }
const char* const c_str() const { return m_query.c_str(); }
private:
StrWPF m_query;
StrWPF m_paramBuf;
vector<size_t> m_paramIndices;
vector<const char*> m_paramValues;
};
#endif

View file

@ -27,26 +27,37 @@
/* From stack overflow: snprintf with an expanding buffer.
*/
bool
StrWPF::catf( const char* fmt, va_list ap )
{
bool success = false;
const int origsiz = size();
resize( origsiz + m_addsiz );
int len = vsnprintf( (char*)c_str() + origsiz, m_addsiz, fmt, ap );
if ( len >= m_addsiz ) { // needs more space
m_addsiz = len + 1;
resize( origsiz );
} else if ( -1 == len ) {
assert(0); // should be impossible
} else {
resize( origsiz + len );
m_addsiz = 100;
success = true;
}
return success;
}
void
StrWPF::catf( const char* fmt, ... )
{
const int origsiz = size();
int addsiz = 100;
va_list ap;
for ( ; ; ) {
resize( origsiz + addsiz );
bool done;
do {
va_list ap;
va_start( ap, fmt );
int len = vsnprintf( (char *)c_str() + origsiz, addsiz, fmt, ap );
done = catf( fmt, ap );
va_end( ap );
if ( len >= addsiz ) { // needs more space
addsiz = len + 1;
} else if ( -1 == len ) {
assert(0); // should be impossible
} else {
resize( origsiz + len );
break;
}
}
} while ( !done );
}

View file

@ -21,10 +21,16 @@
#define _STRWPF_H_
#include <string>
#include <stdarg.h>
class StrWPF : public std::string {
class StrWPF : public std::string {
public:
StrWPF() : m_addsiz(100){}
void catf( const char* fmt, ... );
bool catf( const char* fmt, va_list ap );
private:
int m_addsiz;
};
#endif