From c425686589656ff45f88a883a733d95b6f9b00c2 Mon Sep 17 00:00:00 2001 From: Eric House Date: Mon, 23 Jan 2012 20:52:04 -0800 Subject: [PATCH] lots of changes. Respond to new-bt-game button by posting a list of known paired-with-Crosswords devices and sending invite to the one selected. Includes rescan button with infinite progress indicator (that sometimes works). Recipient saves new game with the gameID passed. Then sender does too (but gameID doesn't seem to stick.) Both games crash when you open them, but they're created. --- .../android/XWords4/res/values/strings.xml | 5 + .../org/eehouse/android/xw4/BTConnection.java | 259 ++++++++++++++++-- .../org/eehouse/android/xw4/GameUtils.java | 24 +- .../eehouse/android/xw4/NewGameActivity.java | 93 ++++++- 4 files changed, 342 insertions(+), 39 deletions(-) diff --git a/xwords4/android/XWords4/res/values/strings.xml b/xwords4/android/XWords4/res/values/strings.xml index 11211b441..499385a49 100644 --- a/xwords4/android/XWords4/res/values/strings.xml +++ b/xwords4/android/XWords4/res/values/strings.xml @@ -1838,4 +1838,9 @@ Turn Bluetooth on + Rescan + Opponent devices + + Scanning for Crosswords on paired devices + diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/BTConnection.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/BTConnection.java index 7607b3884..1d210159a 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/BTConnection.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/BTConnection.java @@ -25,17 +25,27 @@ import android.bluetooth.BluetoothServerSocket; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothDevice; +import android.app.ProgressDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.os.AsyncTask; import android.os.Handler; import java.io.InputStream; import java.io.OutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.util.HashMap; +import java.util.Iterator; import java.util.Set; import java.util.UUID; public class BTConnection extends BroadcastReceiver { public static final int GOT_PONG = 1; + public static final int CONNECT_ACCEPTED = 2; + public static final int CONNECT_REFUSED = 3; + public static final int CONNECT_FAILED = 4; + public static final byte SCAN_DONE = 5; public interface BTStateChangeListener { public void stateChanged( boolean nowEnabled ); @@ -44,16 +54,29 @@ public class BTConnection extends BroadcastReceiver { private static final byte PING = 1; private static final byte PONG = 2; + private static final byte INVITE = 3; + private static final byte INVITE_ACCPT = 4; + private static final byte INVITE_DECL = 5; + private static final byte MESG_SEND = 6; + private static final byte MESG_ACCPT = 7; + private static final byte MESG_DECL = 8; private static BluetoothAdapter s_btAdapter = BluetoothAdapter.getDefaultAdapter(); private static BluetoothServerSocket s_serverSocket; + private static HashMap s_names = + new HashMap(); private class BTListener extends Thread { + private Context m_context; + + public BTListener( Context context ) + { + m_context = context; + } @Override public void run() { - byte[] buffer = new byte[1024]; try { s_serverSocket = s_btAdapter. listenUsingRfcommWithServiceRecord( XWApp.getAppName(), @@ -66,22 +89,28 @@ public class BTConnection extends BroadcastReceiver { while ( null != s_serverSocket ) { BluetoothSocket socket = null; - InputStream inStream = null; + DataInputStream inStream = null; int nRead = 0; try { DbgUtils.logf( "run: calling accept()" ); socket = s_serverSocket.accept(); // blocks DbgUtils.logf( "run: accept() returned" ); - inStream = socket.getInputStream(); - nRead = inStream.read( buffer ); - DbgUtils.logf( "read %d bytes from BT socket", nRead ); + inStream = new DataInputStream( socket.getInputStream() ); - // now handle what's on the socket - if ( 0 < nRead && buffer[0] == PING ) { - DbgUtils.logf( "got PING!!!" ); - OutputStream os = socket.getOutputStream(); - os.write( PONG ); - os.flush(); + short len = inStream.readShort(); + if ( 1 <= len ) { + byte msg = inStream.readByte(); + switch( msg ) { + case PING: + receivePing( socket ); + break; + case INVITE: + receiveInvitation( m_context, inStream, socket ); + break; + default: + DbgUtils.logf( "unexpected msg %d", msg ); + break; + } } socket.close(); @@ -90,7 +119,6 @@ public class BTConnection extends BroadcastReceiver { DbgUtils.logf( "trying again..." ); continue; } - } if ( null != s_serverSocket ) { @@ -122,7 +150,7 @@ public class BTConnection extends BroadcastReceiver { break; case BluetoothAdapter.STATE_ON: asString = "STATE_ON"; - m_listener = new BTListener(); + m_listener = new BTListener( context ); m_listener.start(); tellStateChanged( true ); break; @@ -159,25 +187,17 @@ public class BTConnection extends BroadcastReceiver { for ( BluetoothDevice dev : pairedDevs ) { BluetoothSocket socket = null; try { - socket = dev.createRfcommSocketToServiceRecord( myUUID ); DbgUtils.logf( "PingThread: got socket to device %s", dev.getName() ); - socket.connect(); - DbgUtils.logf( "PingThread: connected" ); - OutputStream os = socket.getOutputStream(); - os.write( PING ); - DbgUtils.logf( "PingThread: wrote" ); - os.flush(); - - InputStream is = socket.getInputStream(); - byte[] buffer = new byte[128]; - int nRead = is.read( buffer ); - if ( 1 == nRead && buffer[0] == PONG ) { - m_handler.obtainMessage( GOT_PONG, dev.getName() ) - .sendToTarget(); + if ( sendPing( dev ) ) { + synchronized( s_names ) { + s_names.put( dev.getName(), dev.getAddress() ); + } + if ( null != m_handler ) { + m_handler.obtainMessage( GOT_PONG, dev.getName() ) + .sendToTarget(); + } } - - socket.close(); } catch ( java.io.IOException ioe ) { DbgUtils.logf( "PingThread: %s", ioe.toString() ); } @@ -201,9 +221,190 @@ public class BTConnection extends BroadcastReceiver { s_stateChangeListener = li; } + // + public static void enqueueFor( byte[] buf ) + { + } + + private static class InviteThread extends Thread { + String m_name; + int m_gameID; + Handler m_handler; + public InviteThread( String name, int gameID, Handler handler ) { + m_name = name; + m_gameID = gameID; + m_handler = handler; + } + + public void run() { + String addr; + int result = CONNECT_FAILED; + synchronized( s_names ) { + addr = s_names.get( m_name ); + } + if ( null != addr ) { + try { + BluetoothDevice remote = s_btAdapter.getRemoteDevice( addr ); + if ( null != remote && sendInvitation( remote, m_gameID ) ) { + result = CONNECT_ACCEPTED; + } + } catch( java.io.IOException ioe ) { + DbgUtils.logf( "ioe: %s", ioe.toString() ); + } + + } + m_handler.obtainMessage( result, m_gameID ).sendToTarget(); + } + } + + public static void inviteRemote( String devName, int gameID, + Handler handler ) + { + InviteThread thread = new InviteThread( devName, gameID, handler ); + thread.start(); + } + + private static class BTScanner extends AsyncTask { + private Handler m_handler; + private ProgressDialog m_progress; + + public BTScanner( Context context, Handler handler ) { + super(); + m_handler = handler; + + String msg = context.getString( R.string.scan_progress ); + m_progress = ProgressDialog.show( context, msg, null, true, true ); + } + + @Override + protected Void doInBackground( Void... unused ) + { + synchronized( s_names ) { + s_names.clear(); + } + new PingThread( null ).run(); // same thread + return null; + } + + @Override + protected void onPostExecute( Void unused ) + { + m_progress.cancel(); + m_handler.obtainMessage( SCAN_DONE ).sendToTarget(); + } + } + + public static void rescan( Context context, Handler handler ) + { + new BTScanner( context, handler ).execute(); + } + + public static String[] listPairedWithXwords() + { + Set names = null; + synchronized( s_names ) { + names = s_names.keySet(); + } + + String[] result = new String[names.size()]; + Iterator iter = names.iterator(); + for ( int ii = 0; iter.hasNext(); ++ii ) { + result[ii] = iter.next(); + } + + return result; + } + private void tellStateChanged( boolean nowOn ) { if ( null != s_stateChangeListener ) { s_stateChangeListener.stateChanged( nowOn ); } } + + private static boolean sendInvitation( BluetoothDevice dev, int gameID ) + throws java.io.IOException + { + boolean success = false; + BluetoothSocket socket = + dev.createRfcommSocketToServiceRecord( XWApp.getAppUUID() ); + if ( null != socket ) { + socket.connect(); + DataOutputStream outStream = + new DataOutputStream( socket.getOutputStream() ); + short len = 1 // INVITE + + 4 // gameID + ; + writeHeader( outStream, len, INVITE ); + // outStream.writeShort( len ); + // outStream.writeByte( INVITE ); + outStream.writeInt( gameID ); + outStream.flush(); + + DataInputStream inStream = + new DataInputStream( socket.getInputStream() ); + success = INVITE_ACCPT == inStream.readByte(); + socket.close(); + } + return success; + } + + private static void receiveInvitation( Context context, + DataInputStream is, + BluetoothSocket socket ) + throws java.io.IOException + { + int gameID = is.readInt(); + DbgUtils.logf( "receiveInvitation: got gameID of %d", gameID ); + + GameUtils.makeNewBTGame( context, gameID ); + + // Post notification that, when selected, will create a game + // -- or ask if user wants to create one. + + OutputStream os = socket.getOutputStream(); + os.write( INVITE_ACCPT ); + os.flush(); + } + + private static boolean sendPing( BluetoothDevice dev ) + throws java.io.IOException + { + boolean success = false; + BluetoothSocket socket = + dev.createRfcommSocketToServiceRecord( XWApp.getAppUUID() ); + if ( null != socket ) { + socket.connect(); + + DbgUtils.logf( "PingThread: connected" ); + DataOutputStream os = new DataOutputStream( socket.getOutputStream() ); + writeHeader( os, (short)1, PING ); + DbgUtils.logf( "PingThread: wrote" ); + os.flush(); + + DataInputStream is = + new DataInputStream( socket.getInputStream() ); + if ( PONG == is.readByte() ) { + success = true; + } + socket.close(); + } + return success; + } + + private static void receivePing( BluetoothSocket socket ) + throws java.io.IOException + { + DbgUtils.logf( "got PING!!!" ); + OutputStream os = socket.getOutputStream(); + os.write( PONG ); + os.flush(); + } + + private static void writeHeader( DataOutputStream outStream, short len, + byte msg ) + throws java.io.IOException + { + outStream.writeShort( len ); + outStream.writeByte( msg ); + } } diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java index ac6ebacd5..6d9a42b29 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/GameUtils.java @@ -381,8 +381,9 @@ public class GameUtils { } private static long makeNewMultiGame( Context context, CommsAddrRec addr, - int[] lang, int nPlayersT, - int nPlayersH, String inviteID ) + int[] lang, + int nPlayersT, int nPlayersH, + String inviteID, int gameID ) { long rowid = -1; @@ -391,6 +392,9 @@ public class GameUtils { lang[0] = gi.dictLang; gi.setNPlayers( nPlayersT, nPlayersH ); gi.juggle(); + if ( 0 != gameID ) { + gi.gameID = gameID; + } // Will need to add a setNPlayers() method to gi to make this // work Assert.assertTrue( gi.nPlayers == nPlayersT ); @@ -414,7 +418,7 @@ public class GameUtils { addr.ip_relay_invite = room; return makeNewMultiGame( context, addr, lang, nPlayersT, - nPlayersH, inviteID ); + nPlayersH, inviteID, 0 ); } public static long makeNewNetGame( Context context, String room, @@ -430,7 +434,7 @@ public class GameUtils { info.nPlayers ); } - public static long makeNewBTGame( Context context, boolean fixme ) + public static long makeNewBTGame( Context context, int gameID ) { long rowid = -1; CommsAddrRec addr = @@ -438,7 +442,7 @@ public class GameUtils { CommsAddrRec.CommsConnType.COMMS_CONN_BT ); int[] lang = { 1 }; // English - return makeNewMultiGame( context, addr, lang, 2, 1, null ); + return makeNewMultiGame( context, addr, lang, 2, 1, null, gameID ); } public static void launchInviteActivity( Context context, @@ -758,6 +762,16 @@ public class GameUtils { return String.format( "%X", rint ).substring( 0, 4 ); } + public static int newGameID() + { + int rint = 0; + while ( 0 == rint ) { + rint = s_random.nextInt(); + } + DbgUtils.logf( "newGameID=>%d", rint ); + return rint; + } + private static void tellRelayDied( Context context, GameLock lock, boolean informNow ) { diff --git a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java index e521b7ada..c0124b8cc 100644 --- a/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java +++ b/xwords4/android/XWords4/src/org/eehouse/android/xw4/NewGameActivity.java @@ -21,11 +21,16 @@ package org.eehouse.android.xw4; import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; import android.bluetooth.BluetoothAdapter; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.TextView; @@ -42,9 +47,11 @@ public class NewGameActivity extends XWActivity implements BTConnection.BTStateChangeListener { private static final int NEW_GAME_ACTION = 1; - private static final int REQUEST_ENABLE_BT = 2; + + private static final int PICK_BTDEV_DLG = DlgDelegate.DIALOG_LAST + 1; private boolean m_showsOn; + private Handler m_handler = null; @Override protected void onCreate(Bundle savedInstanceState) @@ -93,6 +100,55 @@ public class NewGameActivity extends XWActivity checkEnableBT( true ); } + @Override + protected Dialog onCreateDialog( final int id ) + { + Dialog dialog = super.onCreateDialog( id ); + if ( null == dialog ) { + AlertDialog.Builder ab; + DialogInterface.OnClickListener lstnr; + + switch( id ) { + case PICK_BTDEV_DLG: + DialogInterface.OnClickListener scanLstnr = + new DialogInterface.OnClickListener() { + public void onClick( DialogInterface dlg, + int whichButton ) { + BTConnection.rescan( NewGameActivity.this, + getHandler() ); + dlg.dismiss(); + } + }; + ab = new AlertDialog.Builder( this ) + .setTitle( R.string.bt_pick_title ) + .setNegativeButton( R.string.bt_pick_rescan_button, + scanLstnr ); + final String[] btDevs = BTConnection.listPairedWithXwords(); + if ( null != btDevs && 0 < btDevs.length ) { + DialogInterface.OnClickListener devChosenLstnr = + new DialogInterface.OnClickListener() { + public void onClick( DialogInterface dlg, + int whichButton ) { + if ( 0 <= whichButton && whichButton + < btDevs.length ) { + int gameID = GameUtils.newGameID(); + BTConnection. + inviteRemote( btDevs[whichButton], + gameID, + getHandler() ); + } + } + }; + ab.setItems( btDevs, devChosenLstnr ); + } + dialog = ab.create(); + Utils.setRemoveOnDismiss( this, dialog, PICK_BTDEV_DLG ); + break; + } + } + return dialog; + } + // DlgDelegate.DlgClickNotify interface @Override public void dlgButtonClicked( int id, int which ) @@ -170,8 +226,12 @@ public class NewGameActivity extends XWActivity private void makeNewBTGame( boolean useDefaults ) { - GameUtils.makeNewBTGame( this, useDefaults ); - finish(); + int gameID = GameUtils.newGameID(); + if ( useDefaults ) { + showDialog( PICK_BTDEV_DLG ); + } else { + Utils.notImpl( this ); + } } private void checkEnableBT( boolean force ) @@ -209,11 +269,34 @@ public class NewGameActivity extends XWActivity public void onClick( View v ) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); - startActivityForResult( enableBtIntent, - REQUEST_ENABLE_BT ); + startActivityForResult( enableBtIntent, 0 ); } } ); } } } + + private Handler getHandler() + { + if ( null == m_handler ) { + m_handler = new Handler() { + public void handleMessage( Message msg ) { + switch( msg.what ) { + case BTConnection.CONNECT_ACCEPTED: + GameUtils.makeNewBTGame( NewGameActivity.this, + msg.arg1 ); + finish(); + break; + case BTConnection.CONNECT_REFUSED: + case BTConnection.CONNECT_FAILED: + break; + case BTConnection.SCAN_DONE: + showDialog( PICK_BTDEV_DLG ); + break; + } + } + }; + } + return m_handler; + } }