Hedgeroid: Started to implement RoomActivity
authorMedo <smaxein@googlemail.com>
Thu, 02 Aug 2012 16:36:11 +0200
changeset 7461 38acbfdb484f
parent 7458 fec6fa1e460e
child 7464 859ab6859854
Hedgeroid: Started to implement RoomActivity
project_files/Android-build/SDL-android-project/AndroidManifest.xml
project_files/Android-build/SDL-android-project/res/layout/activity_netroom.xml
project_files/Android-build/SDL-android-project/res/values/strings.xml
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyActivity.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Netplay.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/NetplayStateFragment.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Player.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerList.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerListAdapter.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerlistFragment.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomActivity.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomList.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomListAdapter.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomlistFragment.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/TextInputDialog.java
--- a/project_files/Android-build/SDL-android-project/AndroidManifest.xml	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/AndroidManifest.xml	Thu Aug 02 16:36:11 2012 +0200
@@ -72,5 +72,11 @@
             android:screenOrientation="landscape"
             android:windowSoftInputMode="adjustPan" >
         </activity>
+        <activity
+            android:name=".netplay.RoomActivity"
+            android:label="@string/title_activity_room"
+            android:screenOrientation="landscape"
+            android:windowSoftInputMode="adjustPan" >
+        </activity>
     </application>
 </manifest>
\ No newline at end of file
--- a/project_files/Android-build/SDL-android-project/res/layout/activity_netroom.xml	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/res/layout/activity_netroom.xml	Thu Aug 02 16:36:11 2012 +0200
@@ -30,10 +30,10 @@
                 android:layout_marginRight="10dp"
                 android:background="@drawable/box" >
 
-                <fragment
+                <!-- <fragment
                     android:id="@+id/mapFragment"
                     class="org.hedgewars.hedgeroid.netplay.MapFragment"
-                    tools:layout="@layout/fragment_map" />
+                    tools:layout="@layout/fragment_map" /> -->
             </FrameLayout>
 
             <FrameLayout
@@ -44,10 +44,10 @@
                 android:layout_weight="1"
                 android:background="@drawable/box" >
 
-                <fragment
+               <!-- <fragment
                     android:id="@+id/settingsFragment"
                     class="org.hedgewars.hedgeroid.netplay.SettingsFragment"
-                    tools:layout="@layout/fragment_settings" />
+                    tools:layout="@layout/fragment_settings" /> -->
             </FrameLayout>
 
             <FrameLayout
@@ -57,10 +57,10 @@
                 android:layout_weight="1"
                 android:background="@drawable/box" >
 
-                <fragment
+                <!-- <fragment
                     android:id="@+id/teamsFragment"
                     class="org.hedgewars.hedgeroid.netplay.TeamsFragment"
-                    tools:layout="@layout/fragment_teamlist" />
+                    tools:layout="@layout/fragment_teamlist" /> -->
 
             </FrameLayout>
         </LinearLayout>
@@ -74,12 +74,12 @@
             android:layout_below="@id/upperFrame"
             android:background="@drawable/box" >
 
-            <fragment
+            <!-- <fragment
                 android:id="@+id/playerListFragment"
                 android:layout_width="fill_parent"
                 android:layout_height="fill_parent"
                 class="org.hedgewars.hedgeroid.netplay.PlayerlistFragment"
-                tools:layout="@layout/lobby_players_fragment" />
+                tools:layout="@layout/lobby_players_fragment" /> -->
         </FrameLayout>
 
         <FrameLayout
--- a/project_files/Android-build/SDL-android-project/res/values/strings.xml	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/res/values/strings.xml	Thu Aug 02 16:36:11 2012 +0200
@@ -62,6 +62,8 @@
     <string name="bot1">Level 1</string>
 
     <string name="title_activity_lobby">Hedgewars Server Lobby</string>
+    <string name="title_activity_room">Room</string>
+    
     <string name="chat_hint">Type here to chat</string>
 
     <!-- Map settings -->
@@ -118,7 +120,7 @@
     <string name="error_unexpected">An unexpected error has occurred: %1$s</string>
     <string name="error_server_too_old">The server you tried to connect to is using an incompatible protocol.</string>
     <string name="error_auth_failed">Unable to authenticate for your username.</string>
-    <string name="error_connection_lost">The connection to the server was lost: %1$s</string>
+    <string name="error_connection_lost">The connection to the server was lost.</string>
     
     <!-- Dialogs -->
     <string name="dialog_connecting_title">Please wait</string>
@@ -127,4 +129,10 @@
     <string name="dialog_password_message">The server has requested a password to connect as "%1$s".</string>
     <string name="dialog_password_hint">Password</string>
     <string name="dialog_password_remember">remember password</string>
+    <string name="dialog_create_room_hint">Room name</string>
+    <string name="dialog_create_room_title">Create new room</string>
+    
+    <string name="toast_disconnected">Disconnected: %1$s</string>
+    <string name="toast_room_abandoned">The room was closed because the owner left.</string>
+    <string name="toast_kicked">You were kicked from the room.</string>
 </resources>
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyActivity.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyActivity.java	Thu Aug 02 16:36:11 2012 +0200
@@ -1,8 +1,13 @@
 package org.hedgewars.hedgeroid.netplay;
 
 import org.hedgewars.hedgeroid.R;
+import org.hedgewars.hedgeroid.StartGameActivity;
+import org.hedgewars.hedgeroid.netplay.Netplay.State;
+import org.hedgewars.hedgeroid.netplay.NetplayStateFragment.NetplayStateListener;
+import org.hedgewars.hedgeroid.netplay.TextInputDialog.TextInputDialogListener;
 
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.support.v4.app.FragmentActivity;
@@ -15,9 +20,10 @@
 import android.widget.LinearLayout;
 import android.widget.TabHost;
 import android.widget.TextView;
-import android.widget.Toast;
 
-public class LobbyActivity extends FragmentActivity {
+public class LobbyActivity extends FragmentActivity implements TextInputDialogListener, NetplayStateListener {
+	private static final int DIALOG_CREATE_ROOM = 0;
+	
     private TabHost tabHost;
     private Netplay netplay;
     
@@ -79,7 +85,8 @@
 	public boolean onOptionsItemSelected(MenuItem item) {
 		switch(item.getItemId()) {
 		case R.id.room_create:
-			Toast.makeText(this, R.string.not_implemented_yet, Toast.LENGTH_SHORT).show();
+	        TextInputDialog dialog = new TextInputDialog(DIALOG_CREATE_ROOM, R.string.dialog_create_room_title, 0, R.string.dialog_create_room_hint);
+	        dialog.show(getSupportFragmentManager(), "create_room_dialog");
 			return true;
 		case R.id.disconnect:
 			netplay.disconnect();
@@ -102,4 +109,31 @@
         	icicle.putString("currentTab", tabHost.getCurrentTabTag());
         }
     }
+    
+    public void onTextInputDialogSubmitted(int dialogId, String text) {
+    	if(text != null && text.length()>0) {
+    		netplay.sendCreateRoom(text);
+    	}
+    }
+    
+    public void onTextInputDialogCancelled(int dialogId) {
+    }
+    
+    public void onNetplayStateChanged(State newState) {
+    	switch(newState) {
+    	case CONNECTING:
+    	case NOT_CONNECTED:
+    		finish();
+    		break;
+    	case ROOM:
+    	case INGAME:
+    		startActivity(new Intent(getApplicationContext(), RoomActivity.class));
+    		break;
+    	case LOBBY:
+    		// Do nothing
+    		break;
+		default:
+			throw new IllegalStateException("Unknown connection state: "+newState);
+    	}
+    }
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Netplay.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Netplay.java	Thu Aug 02 16:36:11 2012 +0200
@@ -5,6 +5,7 @@
 
 import org.hedgewars.hedgeroid.R;
 import org.hedgewars.hedgeroid.Utils;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.BoolCallback;
 import org.hedgewars.hedgeroid.netplay.JnaFrontlib.IntStrCallback;
 import org.hedgewars.hedgeroid.netplay.JnaFrontlib.MetaschemePtr;
 import org.hedgewars.hedgeroid.netplay.JnaFrontlib.NetconnPtr;
@@ -40,11 +41,15 @@
 	public static final String EXTRA_PLAYERNAME = "playerName";
 	public static final String EXTRA_MESSAGE = "message";
 	public static final String EXTRA_HAS_ERROR = "hasError";
+	public static final String EXTRA_REASON = "reason";
 	
 	private static final String ACTIONPREFIX = "org.hedgewars.hedgeroid.netconn.";
 	public static final String ACTION_DISCONNECTED = ACTIONPREFIX+"DISCONNECTED";
 	public static final String ACTION_CONNECTED = ACTIONPREFIX+"CONNECTED";
 	public static final String ACTION_PASSWORD_REQUESTED = ACTIONPREFIX+"PASSWORD_REQUESTED";
+	public static final String ACTION_ENTERED_ROOM_FROM_LOBBY = ACTIONPREFIX+"ENTERED_ROOM";
+	public static final String ACTION_LEFT_ROOM = ACTIONPREFIX+"LEFT_ROOM";
+	public static final String ACTION_STATE_CHANGED = ACTIONPREFIX+"STATE_CHANGED";
 	
 	public static final String DEFAULT_SERVER = "netserver.hedgewars.org";
 	public static final int DEFAULT_PORT = 46631;
@@ -54,13 +59,14 @@
 	private final FromNetHandler fromNetHandler = new FromNetHandler();
 	
 	private State state;
-	private int foregroundUsers = 0;
+	private int foregroundUsers = 0;	// Reference counter of clients requesting foreground tick speed (fast ticks)
+	private boolean chief;				// Do we control the current room?
 	
 	// null if there is no running connection (==state is NOT_CONNECTED)
 	private ThreadedNetConnection connection;
 	
-	public final PlayerList playerList = new PlayerList();
-	public final RoomList roomList = new RoomList();
+	public final Playerlist playerList = new Playerlist();
+	public final Roomlist roomList = new Roomlist();
 	public final MessageLog lobbyChatlog;
 	public final MessageLog roomChatlog;
 	
@@ -96,7 +102,7 @@
 		}
 		
 		clearState();
-		state = State.CONNECTING;
+		changeState(State.CONNECTING);
 		connection = ThreadedNetConnection.startConnection(appContext, fromNetHandler, name, host, port);
 		connection.setFastTickRate(foregroundUsers > 0);
 	}
@@ -106,7 +112,12 @@
 	public void sendQuit(String message) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_QUIT, message); }
 	public void sendRoomlistRequest() { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_ROOMLIST_REQUEST); }
 	public void sendPlayerInfoQuery(String name) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_PLAYER_INFO_REQUEST, name); }
-	public void sendChat(final String s) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_CHAT, s); }
+	public void sendChat(String s) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_CHAT, s); }
+	public void sendFollowPlayer(String nick) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_FOLLOW_PLAYER, nick); }
+	public void sendJoinRoom(String name) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_JOIN_ROOM, name); }
+	public void sendCreateRoom(String name) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_CREATE_ROOM, name); }
+	public void sendLeaveRoom(String message) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_LEAVE_ROOM, message); }
+	
 	public void disconnect() { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_DISCONNECT, "User Quit"); }
 	
 	private static Netplay instance;
@@ -133,6 +144,17 @@
 		return state;
 	}
 	
+	private void changeState(State newState) {
+		if(newState != state) {
+			state = newState;
+			broadcastManager.sendBroadcastSync(new Intent(ACTION_STATE_CHANGED));
+		}
+	}
+	
+	public boolean isChief() {
+		return chief;
+	}
+	
 	/**
 	 * Indicate that you want network messages to be checked regularly (several times per second).
 	 * As long as nobody requests fast ticks, the network is only checked once every few seconds
@@ -191,15 +213,19 @@
 	final class FromNetHandler extends Handler {
 		public static final int MSG_LOBBY_JOIN = 0;
 		public static final int MSG_LOBBY_LEAVE = 1;
-		public static final int MSG_CHAT = 2;
-		public static final int MSG_MESSAGE = 3;
-		public static final int MSG_ROOM_ADD = 4;
-		public static final int MSG_ROOM_UPDATE = 5;
-		public static final int MSG_ROOM_DELETE = 6;
-		public static final int MSG_ROOMLIST = 7;
-		public static final int MSG_CONNECTED = 8;
-		public static final int MSG_DISCONNECTED = 9;
-		public static final int MSG_PASSWORD_REQUEST = 10;
+		public static final int MSG_ROOM_JOIN = 2;
+		public static final int MSG_ROOM_LEAVE = 3;
+		public static final int MSG_CHAT = 4;
+		public static final int MSG_MESSAGE = 5;
+		public static final int MSG_ROOM_ADD = 6;
+		public static final int MSG_ROOM_UPDATE = 7;
+		public static final int MSG_ROOM_DELETE = 8;
+		public static final int MSG_ROOMLIST = 9;
+		public static final int MSG_CONNECTED = 10;
+		public static final int MSG_DISCONNECTED = 11;
+		public static final int MSG_PASSWORD_REQUEST = 12;
+		public static final int MSG_ENTER_ROOM_FROM_LOBBY = 13;
+		public static final int MSG_LEAVE_ROOM = 14;
 		
 		public FromNetHandler() {
 			super(Looper.getMainLooper());
@@ -221,6 +247,18 @@
 				lobbyChatlog.appendPlayerLeave(args.first, args.second);
 				break;
 			}
+			case MSG_ROOM_JOIN: {
+				String name = (String)msg.obj;
+				// TODO roomPlayerList.addPlayerWithNewId(name);
+				roomChatlog.appendPlayerJoin(name);
+				break;
+			}
+			case MSG_ROOM_LEAVE: {
+				Pair<String, String> args = (Pair<String, String>)msg.obj;
+				// TODO roomPlayerList.removePlayer(args.first);
+				roomChatlog.appendPlayerLeave(args.first, args.second);
+				break;
+			}
 			case MSG_CHAT: {
 				Pair<String, String> args = (Pair<String, String>)msg.obj;
 				getCurrentLog().appendChat(args.first, args.second);
@@ -248,13 +286,13 @@
 				break;
 			}
 			case MSG_CONNECTED: {
-				state = State.LOBBY;
+				changeState(State.LOBBY);
 				broadcastManager.sendBroadcast(new Intent(ACTION_CONNECTED));
 				break;
 			}
 			case MSG_DISCONNECTED: {
 				Pair<Boolean, String> args = (Pair<Boolean, String>)msg.obj;
-				state = State.NOT_CONNECTED;
+				changeState(State.NOT_CONNECTED);
 				connection = null;
 				Intent intent = new Intent(ACTION_DISCONNECTED);
 				intent.putExtra(EXTRA_HAS_ERROR, args.first);
@@ -268,6 +306,22 @@
 				broadcastManager.sendBroadcast(intent);
 				break;
 			}
+			case MSG_ENTER_ROOM_FROM_LOBBY: {
+				roomChatlog.clear();
+				changeState(State.ROOM);
+				chief = (Boolean)msg.obj;
+				Intent intent = new Intent(ACTION_ENTERED_ROOM_FROM_LOBBY);
+				broadcastManager.sendBroadcastSync(intent);
+				break;
+			}
+			case MSG_LEAVE_ROOM: {
+				changeState(State.LOBBY);
+				Intent intent = new Intent(ACTION_LEFT_ROOM);
+				intent.putExtra(EXTRA_MESSAGE, (String)msg.obj);
+				intent.putExtra(EXTRA_REASON, msg.arg1);
+				broadcastManager.sendBroadcastSync(intent);
+				break;
+			}
 			default: {
 				Log.e("FromNetHandler", "Unknown message type: "+msg.what);
 				break;
@@ -332,6 +386,8 @@
 					}
 					FLIB.flib_netconn_onLobbyJoin(conn, lobbyJoinCb, null);
 					FLIB.flib_netconn_onLobbyLeave(conn, lobbyLeaveCb, null);
+					FLIB.flib_netconn_onRoomJoin(conn, roomJoinCb, null);
+					FLIB.flib_netconn_onRoomLeave(conn, roomLeaveCb, null);
 					FLIB.flib_netconn_onChat(conn, chatCb, null);
 					FLIB.flib_netconn_onMessage(conn, messageCb, null);
 					FLIB.flib_netconn_onRoomAdd(conn, roomAddCb, null);
@@ -341,6 +397,9 @@
 					FLIB.flib_netconn_onRoomlist(conn, roomlistCb, null);
 					FLIB.flib_netconn_onDisconnected(conn, disconnectCb, null);
 					FLIB.flib_netconn_onPasswordRequest(conn, passwordRequestCb, null);
+					FLIB.flib_netconn_onEnterRoom(conn, enterRoomCb, null);
+					FLIB.flib_netconn_onLeaveRoom(conn, leaveRoomCb, null);
+					
 					FLIB.flib_metascheme_release(meta);
 					tickHandler.start();
 				}
@@ -375,6 +434,16 @@
 			}
 		};
 		
+		private final StrCallback roomJoinCb = new StrCallback() {
+			public void callback(Pointer context, String name) {
+				sendFromNet(FromNetHandler.MSG_ROOM_JOIN, name);
+			}
+		};
+		private final StrStrCallback roomLeaveCb = new StrStrCallback() {
+			public void callback(Pointer context, String name, String message) {
+				sendFromNet(FromNetHandler.MSG_ROOM_LEAVE, Pair.create(name, message));
+			}
+		};
 		private final StrStrCallback chatCb = new StrStrCallback() {
 			public void callback(Pointer context, String name, String msg) {
 				sendFromNet(FromNetHandler.MSG_CHAT, Pair.create(name, msg));
@@ -425,6 +494,18 @@
 			}
 		};
 		
+		private final BoolCallback enterRoomCb = new BoolCallback() {
+			public void callback(Pointer context, boolean isChief) {
+				sendFromNet(FromNetHandler.MSG_ENTER_ROOM_FROM_LOBBY, Boolean.TRUE);
+			}
+		};
+		
+		private final IntStrCallback leaveRoomCb = new IntStrCallback() {
+			public void callback(Pointer context, int reason, String message) {
+				sendFromNet(FromNetHandler.MSG_LEAVE_ROOM, reason, message);
+			}
+		};
+		
 		private void shutdown(boolean error, String message) {
 			if(conn != null) {
 				FLIB.flib_netconn_destroy(conn);
@@ -448,7 +529,7 @@
 			case JnaFrontlib.NETCONN_DISCONNECT_AUTH_FAILED:
 				return res.getString(R.string.error_auth_failed);
 			case JnaFrontlib.NETCONN_DISCONNECT_CONNLOST:
-				return res.getString(R.string.error_connection_lost, message);
+				return res.getString(R.string.error_connection_lost);
 			case JnaFrontlib.NETCONN_DISCONNECT_INTERNAL_ERROR:
 				return res.getString(R.string.error_unexpected, message);
 			case JnaFrontlib.NETCONN_DISCONNECT_SERVER_TOO_OLD:
@@ -476,7 +557,12 @@
 			public static final int MSG_SEND_ROOMLIST_REQUEST = 3;
 			public static final int MSG_SEND_PLAYER_INFO_REQUEST = 4;
 			public static final int MSG_SEND_CHAT = 5;
-			public static final int MSG_DISCONNECT = 6;
+			public static final int MSG_SEND_FOLLOW_PLAYER = 6;
+			public static final int MSG_SEND_JOIN_ROOM = 7;
+			public static final int MSG_SEND_CREATE_ROOM = 8;
+			public static final int MSG_SEND_LEAVE_ROOM = 9;
+			
+			public static final int MSG_DISCONNECT = 10;
 			
 			public ToNetHandler(Looper looper) {
 				super(looper);
@@ -511,6 +597,24 @@
 					}
 					break;
 				}
+				case MSG_SEND_FOLLOW_PLAYER: {
+					FLIB.flib_netconn_send_playerFollow(conn, (String)msg.obj);
+					break;
+				}
+				case MSG_SEND_JOIN_ROOM: {
+					FLIB.flib_netconn_send_joinRoom(conn, (String)msg.obj);
+					break;
+				}
+				case MSG_SEND_CREATE_ROOM: {
+					FLIB.flib_netconn_send_createRoom(conn, (String)msg.obj);
+					break;
+				}
+				case MSG_SEND_LEAVE_ROOM: {
+					if(FLIB.flib_netconn_send_leaveRoom(conn, (String)msg.obj) == 0) {
+						sendFromNet(FromNetHandler.MSG_LEAVE_ROOM, -1, "");
+					}
+					break;
+				}
 				case MSG_DISCONNECT: {
 					FLIB.flib_netconn_send_quit(conn, (String)msg.obj);
 					shutdown(false, "User quit");
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/NetplayStateFragment.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/NetplayStateFragment.java	Thu Aug 02 16:36:11 2012 +0200
@@ -1,7 +1,9 @@
 package org.hedgewars.hedgeroid.netplay;
 
+import org.hedgewars.hedgeroid.R;
 import org.hedgewars.hedgeroid.netplay.Netplay.State;
 
+import android.app.Activity;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -13,30 +15,45 @@
 
 /**
  * Fragment for use by an activity that depends on the state of the network
- * connection.
+ * connection. The activity must implement the NetplayStateListener interface.
  * 
  * This fragment manages a few aspects of the netplay connection: Requesting
  * the network system loop to run at high frequency while the activity is in
- * the foreground, and reacting to changes in the networking state by switching
- * to the appropriate activity or finishing the activity if the network connection
- * is closed.
+ * the foreground, and reacting to changes in the networking state by calling
+ * a callback method on the activity.
  */
 public class NetplayStateFragment extends Fragment {
     private Netplay netplay;
     private Context appContext;
     private LocalBroadcastManager broadcastManager;
+    private NetplayStateListener listener;
+    private State knownState;
     
-    private final BroadcastReceiver disconnectReceiver = new BroadcastReceiver() {
-		@Override
-		public void onReceive(Context context, Intent intent) {
-			if(intent.getBooleanExtra(Netplay.EXTRA_HAS_ERROR, true)) {
-				String message = intent.getStringExtra(Netplay.EXTRA_MESSAGE);
-				Toast.makeText(appContext, "Disconnected: "+message, Toast.LENGTH_LONG).show();
-			}
-			getActivity().finish();
+    interface NetplayStateListener {
+    	/**
+    	 * This is called while the activity is running, and every time during resume, if
+    	 * a change in the networking state is detected. It is also called once
+    	 * with the initial state (which could be called a change from the "unknown" state).
+    	 */
+    	void onNetplayStateChanged(State newState);
+    }
+    
+    @Override
+	public void onAttach(Activity activity) {
+		super.onAttach(activity);
+		try {
+			listener = (NetplayStateListener) activity;
+		} catch(ClassCastException e) {
+			throw new ClassCastException("Activity " + activity + " must implement NetplayStateListener to use NetplayStateFragment.");
 		}
-	};
-    
+	}
+	
+	@Override
+	public void onDetach() {
+		super.onDetach();
+		listener = null;
+	}
+	
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -49,10 +66,14 @@
     public void onResume() {
     	super.onResume();
     	broadcastManager.registerReceiver(disconnectReceiver, new IntentFilter(Netplay.ACTION_DISCONNECTED));
+    	broadcastManager.registerReceiver(leaveRoomReceiver, new IntentFilter(Netplay.ACTION_LEFT_ROOM));
+    	broadcastManager.registerReceiver(stateChangeReceiver, new IntentFilter(Netplay.ACTION_STATE_CHANGED));
     	netplay.requestFastTicks();
     	
-    	if(netplay.getState() == State.NOT_CONNECTED) {
-    		getActivity().finish();
+    	State newState = netplay.getState();
+		if(knownState != newState) {
+    		listener.onNetplayStateChanged(newState);
+    		knownState = newState;
     	}
     }
     
@@ -60,6 +81,40 @@
     public void onPause() {
     	super.onPause();
     	broadcastManager.unregisterReceiver(disconnectReceiver);
+    	broadcastManager.unregisterReceiver(leaveRoomReceiver);
+    	broadcastManager.unregisterReceiver(stateChangeReceiver);
     	netplay.unrequestFastTicks();
     }
+
+	private final BroadcastReceiver disconnectReceiver = new BroadcastReceiver() {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			if(intent.getBooleanExtra(Netplay.EXTRA_HAS_ERROR, true)) {
+				String message = intent.getStringExtra(Netplay.EXTRA_MESSAGE);
+				String toastText = getString(R.string.toast_disconnected, message);
+				Toast.makeText(appContext, toastText, Toast.LENGTH_LONG).show();
+			}
+		}
+	};
+	
+	private final BroadcastReceiver leaveRoomReceiver = new BroadcastReceiver() {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			int reason = intent.getIntExtra(Netplay.EXTRA_REASON, -1);
+			if(reason == JnaFrontlib.NETCONN_ROOMLEAVE_ABANDONED) {
+				Toast.makeText(appContext, R.string.toast_room_abandoned, Toast.LENGTH_LONG).show();
+			} else if(reason == JnaFrontlib.NETCONN_ROOMLEAVE_KICKED) {
+				Toast.makeText(appContext, R.string.toast_kicked, Toast.LENGTH_LONG).show();
+			}
+		}
+	};
+	
+	private final BroadcastReceiver stateChangeReceiver = new BroadcastReceiver() {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			State newState = netplay.getState();
+			listener.onNetplayStateChanged(newState);
+			knownState = newState;
+		}
+	};
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Player.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Player.java	Thu Aug 02 16:36:11 2012 +0200
@@ -6,4 +6,4 @@
 	public Player(String name) {
 		this.name = name;
 	}
-}
\ No newline at end of file
+}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerList.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerList.java	Thu Aug 02 16:36:11 2012 +0200
@@ -7,7 +7,7 @@
 import android.database.DataSetObservable;
 import android.util.Pair;
 
-public class PlayerList extends DataSetObservable {
+public class Playerlist extends DataSetObservable {
 	private long nextId = 1;
 	private Map<String, Pair<Player, Long>> players = new TreeMap<String, Pair<Player, Long>>();
 	
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerListAdapter.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerListAdapter.java	Thu Aug 02 16:36:11 2012 +0200
@@ -16,15 +16,15 @@
 import android.widget.BaseAdapter;
 import android.widget.TextView;
 
-public class PlayerListAdapter extends BaseAdapter {
+public class PlayerlistAdapter extends BaseAdapter {
 	private List<Pair<Player, Long>> players = new ArrayList<Pair<Player, Long>>();
 	private Context context;
-	private PlayerList playerList;
+	private Playerlist playerlist;
 	
 	private DataSetObserver observer = new DataSetObserver() {
 		@Override
 		public void onChanged() {
-			reloadFromList(playerList);
+			reloadFromList(playerlist);
 		}
 		
 		@Override
@@ -33,7 +33,7 @@
 		}
 	};
 	
-	public PlayerListAdapter(Context context) {
+	public PlayerlistAdapter(Context context) {
 		this.context = context;
 	}
 	
@@ -53,24 +53,24 @@
 		return true;
 	}
 	
-	public void setList(PlayerList playerList) {
-		if(this.playerList != null) {
-			this.playerList.unregisterObserver(observer);
+	public void setList(Playerlist playerlist) {
+		if(this.playerlist != null) {
+			this.playerlist.unregisterObserver(observer);
 		}
-		this.playerList = playerList;
-		this.playerList.registerObserver(observer);
-		reloadFromList(playerList);
+		this.playerlist = playerlist;
+		this.playerlist.registerObserver(observer);
+		reloadFromList(playerlist);
 	}
 	
 	public void invalidate() {
-		if(playerList != null) {
-			playerList.unregisterObserver(observer);
+		if(playerlist != null) {
+			playerlist.unregisterObserver(observer);
 		}
-		playerList = null;
+		playerlist = null;
 		notifyDataSetInvalidated();
 	}
 	
-	private void reloadFromList(PlayerList list) {
+	private void reloadFromList(Playerlist list) {
 		players = new ArrayList<Pair<Player, Long>>(list.getMap().values());
 		Collections.sort(players, AlphabeticalOrderComparator.INSTANCE);
 		notifyDataSetChanged();
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerlistFragment.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerlistFragment.java	Thu Aug 02 16:36:11 2012 +0200
@@ -12,17 +12,16 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView.AdapterContextMenuInfo;
-import android.widget.Toast;
 
 public class PlayerlistFragment extends ListFragment {
 	private Netplay netconn;
-	private PlayerListAdapter playerListAdapter;
+	private PlayerlistAdapter playerListAdapter;
 	
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
 		netconn = Netplay.getAppInstance(getActivity().getApplicationContext());
-		playerListAdapter = new PlayerListAdapter(getActivity());
+		playerListAdapter = new PlayerlistAdapter(getActivity());
 		playerListAdapter.setList(Netplay.getAppInstance(getActivity().getApplicationContext()).playerList);
 		setListAdapter(playerListAdapter);
 	}
@@ -53,13 +52,13 @@
 	@Override
 	public boolean onContextItemSelected(MenuItem item) {
 		AdapterContextMenuInfo info = (AdapterContextMenuInfo)item.getMenuInfo();
+		Player player = playerListAdapter.getItem(info.position);
 		switch(item.getItemId()) {
 		case R.id.player_info:
-			Player p = playerListAdapter.getItem(info.position);
-			netconn.sendPlayerInfoQuery(p.name);
+			netconn.sendPlayerInfoQuery(player.name);
 			return true;
 		case R.id.player_follow:
-			Toast.makeText(getActivity(), R.string.not_implemented_yet, Toast.LENGTH_SHORT).show();
+			netconn.sendFollowPlayer(player.name);
 			return true;
 		default:
 			return super.onContextItemSelected(item);
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomActivity.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomActivity.java	Thu Aug 02 16:36:11 2012 +0200
@@ -2,29 +2,38 @@
 
 import org.hedgewars.hedgeroid.R;
 import org.hedgewars.hedgeroid.netplay.JnaFrontlib.NetconnPtr;
+import org.hedgewars.hedgeroid.netplay.Netplay.State;
+import org.hedgewars.hedgeroid.netplay.NetplayStateFragment.NetplayStateListener;
 
+import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentTransaction;
 import android.support.v4.content.LocalBroadcastManager;
 import android.widget.LinearLayout;
 import android.widget.TabHost;
+import android.widget.Toast;
 
-public class RoomActivity extends FragmentActivity {
+public class RoomActivity extends FragmentActivity implements NetplayStateListener {
 	private TabHost tabHost;
-	private Netplay netconn;
+	private Netplay netplay;
 	
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
-        netconn = Netplay.getAppInstance(getApplicationContext());
+        netplay = Netplay.getAppInstance(getApplicationContext());
+        
+        setContentView(R.layout.activity_netroom);
+        ChatFragment chatFragment = (ChatFragment)getSupportFragmentManager().findFragmentById(R.id.chatFragment);
+        chatFragment.setInRoom(true);
         
-        setContentView(R.layout.activity_lobby);
-        Fragment chatFragment = getSupportFragmentManager().findFragmentById(R.id.chatFragment);
-        chatFragment.getArguments().putBoolean(ChatFragment.ARGUMENT_INROOM, true);
+        FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
+        trans.add(new NetplayStateFragment(), "netplayFragment");
+        trans.commit();
         
-        tabHost = (TabHost)findViewById(android.R.id.tabhost);
+        /*tabHost = (TabHost)findViewById(android.R.id.tabhost);
         if(tabHost != null) {
 	        tabHost.setup();
 	        tabHost.getTabWidget().setOrientation(LinearLayout.VERTICAL);
@@ -35,9 +44,14 @@
 	        if (icicle != null) {
 	            tabHost.setCurrentTabByTag(icicle.getString("currentTab"));
 	        }
-        }
+        }*/
     }
 
+	@Override
+	public void onBackPressed() {
+		netplay.sendLeaveRoom(null);
+	}
+    
     @Override
     protected void onSaveInstanceState(Bundle icicle) {
         super.onSaveInstanceState(icicle);
@@ -45,4 +59,23 @@
         	icicle.putString("currentTab", tabHost.getCurrentTabTag());
         }
     }
+    
+    public void onNetplayStateChanged(State newState) {
+    	switch(newState) {
+    	case NOT_CONNECTED:
+    	case CONNECTING:
+    	case LOBBY:
+    		finish();
+    		break;
+    	case ROOM:
+    		// Do nothing
+    		break;
+    	case INGAME:
+    		//startActivity(new Intent(getApplicationContext(), RoomActivity.class));
+    		Toast.makeText(getApplicationContext(), R.string.not_implemented_yet, Toast.LENGTH_SHORT).show();
+    		break;
+		default:
+			throw new IllegalStateException("Unknown connection state: "+newState);
+    	}
+    }
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomList.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomList.java	Thu Aug 02 16:36:11 2012 +0200
@@ -8,7 +8,7 @@
 import android.util.Log;
 import android.util.Pair;
 
-public class RoomList extends DataSetObservable {
+public class Roomlist extends DataSetObservable {
 	private long nextId = 1;
 	private Map<String, Pair<Room, Long>> rooms = new TreeMap<String, Pair<Room, Long>>();
 	
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomListAdapter.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomListAdapter.java	Thu Aug 02 16:36:11 2012 +0200
@@ -17,15 +17,15 @@
 import android.widget.BaseAdapter;
 import android.widget.TextView;
 
-public class RoomListAdapter extends BaseAdapter {
+public class RoomlistAdapter extends BaseAdapter {
 	private List<Pair<Room, Long>> rooms = new ArrayList<Pair<Room, Long>>();
 	private Context context;
-	private RoomList roomList;
+	private Roomlist roomlist;
 	
 	private DataSetObserver observer = new DataSetObserver() {
 		@Override
 		public void onChanged() {
-			reloadFromList(roomList);
+			reloadFromList(roomlist);
 		}
 		
 		@Override
@@ -34,7 +34,7 @@
 		}
 	};
 	
-	public RoomListAdapter(Context context) {
+	public RoomlistAdapter(Context context) {
 		this.context = context;
 	}
 	
@@ -54,25 +54,25 @@
 		return true;
 	}
 
-	public void setList(RoomList roomList) {
-		if(this.roomList != null) {
-			this.roomList.unregisterObserver(observer);
+	public void setList(Roomlist roomlist) {
+		if(this.roomlist != null) {
+			this.roomlist.unregisterObserver(observer);
 		}
-		this.roomList = roomList;
-		this.roomList.registerObserver(observer);
-		reloadFromList(roomList);
+		this.roomlist = roomlist;
+		this.roomlist.registerObserver(observer);
+		reloadFromList(roomlist);
 	}
 	
 	public void invalidate() {
-		if(roomList != null) {
-			roomList.unregisterObserver(observer);
+		if(roomlist != null) {
+			roomlist.unregisterObserver(observer);
 		}
-		roomList = null;
+		roomlist = null;
 		notifyDataSetInvalidated();
 	}
 	
-	private void reloadFromList(RoomList list) {
-		rooms = new ArrayList<Pair<Room, Long>>(roomList.getMap().values());
+	private void reloadFromList(Roomlist list) {
+		rooms = new ArrayList<Pair<Room, Long>>(roomlist.getMap().values());
 		Collections.sort(rooms, RoomAgeComparator.INSTANCE);
 		notifyDataSetChanged();
 	}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomlistFragment.java	Thu Aug 02 16:35:12 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomlistFragment.java	Thu Aug 02 16:36:11 2012 +0200
@@ -10,17 +10,16 @@
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
-import android.widget.Toast;
 
 public class RoomlistFragment extends ListFragment implements OnItemClickListener {
 	private static final int AUTO_REFRESH_INTERVAL_MS = 15000;
 	
-	private Netplay netconn;
-	private RoomListAdapter adapter;
+	private Netplay netplay;
+	private RoomlistAdapter adapter;
 	private CountDownTimer autoRefreshTimer = new CountDownTimer(Long.MAX_VALUE, AUTO_REFRESH_INTERVAL_MS) {
 		@Override
 		public void onTick(long millisUntilFinished) {
-			netconn.sendRoomlistRequest();
+			netplay.sendRoomlistRequest();
 		}
 		
 		@Override
@@ -30,9 +29,9 @@
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
-		netconn = Netplay.getAppInstance(getActivity().getApplicationContext());
-		adapter = new RoomListAdapter(getActivity());
-		adapter.setList(netconn.roomList);
+		netplay = Netplay.getAppInstance(getActivity().getApplicationContext());
+		adapter = new RoomlistAdapter(getActivity());
+		adapter.setList(netplay.roomList);
 		setListAdapter(adapter);
 	}
 
@@ -67,6 +66,6 @@
 	}
 	
 	public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-		Toast.makeText(getActivity(), R.string.not_implemented_yet, Toast.LENGTH_SHORT).show();
+		netplay.sendJoinRoom(adapter.getItem(position).name);
 	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/TextInputDialog.java	Thu Aug 02 16:36:11 2012 +0200
@@ -0,0 +1,128 @@
+package org.hedgewars.hedgeroid.netplay;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.view.KeyEvent;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+/**
+ * A generic text input dialog with configurable text. The Activity must implement the callback
+ * interface TextInputDialogListener, which will be called by the dialog if it is submitted or cancelled.
+ */
+public class TextInputDialog extends DialogFragment {
+	private static final String BUNDLE_DIALOG_ID = "dialogId";
+	private static final String BUNDLE_TITLE_TEXT = "title";
+	private static final String BUNDLE_MESSAGE_TEXT = "message";
+	private static final String BUNDLE_HINT_TEXT = "hint";
+	
+	private int dialogId, titleText, messageText, hintText;
+	private TextInputDialogListener listener;
+	
+	public interface TextInputDialogListener {
+		void onTextInputDialogSubmitted(int dialogId, String text);
+		void onTextInputDialogCancelled(int dialogId);
+	}
+	
+	/**
+	 * The dialogId is only used for passing back to the callback on the activity, the
+	 * other parameters are text resource IDs. Pass 0 for any of them to not use this
+	 * text.
+	 */
+	public TextInputDialog(int dialogId, int titleText, int messageText, int hintText) {
+		this.dialogId = dialogId;
+		this.titleText = titleText;
+		this.messageText = messageText;
+		this.hintText = hintText;
+	}
+	
+	public TextInputDialog() {
+		// Only for reflection-based instantiation by the framework
+	}
+	
+	@Override
+	public void onAttach(Activity activity) {
+		super.onAttach(activity);
+		try {
+			listener = (TextInputDialogListener) activity;
+		} catch(ClassCastException e) {
+			throw new ClassCastException("Activity " + activity + " must implement TextInputDialogListener to use TextInputDialog.");
+		}
+	}
+	
+	@Override
+	public void onDetach() {
+		super.onDetach();
+		listener = null;
+	}
+	
+	@Override
+	public Dialog onCreateDialog(Bundle savedInstanceState) {
+		if(savedInstanceState != null) {
+			dialogId = savedInstanceState.getInt(BUNDLE_DIALOG_ID, dialogId);
+			titleText = savedInstanceState.getInt(BUNDLE_TITLE_TEXT, titleText);
+			messageText = savedInstanceState.getInt(BUNDLE_MESSAGE_TEXT, messageText);
+			hintText = savedInstanceState.getInt(BUNDLE_HINT_TEXT, hintText);
+		}
+		
+		final EditText editText = new EditText(getActivity());
+		AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+		
+		if(titleText != 0) {
+			builder.setTitle(titleText);
+		}
+		if(messageText != 0) {
+			builder.setTitle(messageText);
+		}
+		if(hintText != 0) {
+			editText.setHint(hintText);
+		}
+		
+		editText.setId(android.R.id.text1);
+		editText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+		editText.setSingleLine();
+
+		builder.setView(editText);
+		builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+			public void onClick(DialogInterface dialog, int which) {
+				dialog.cancel();
+			}
+		});
+		
+		editText.setOnEditorActionListener(new OnEditorActionListener() {
+			public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+				listener.onTextInputDialogSubmitted(dialogId, v.getText().toString());
+				return true;
+			}
+		});
+		
+		builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+			public void onClick(DialogInterface dialog, int which) {
+				listener.onTextInputDialogSubmitted(dialogId, editText.getText().toString());
+			}
+		});
+
+		return builder.create();
+	}
+	
+	@Override
+	public void onSaveInstanceState(Bundle icicle) {
+		super.onSaveInstanceState(icicle);
+		icicle.putInt(BUNDLE_DIALOG_ID, dialogId);
+		icicle.putInt(BUNDLE_TITLE_TEXT, titleText);
+		icicle.putInt(BUNDLE_MESSAGE_TEXT, messageText);
+		icicle.putInt(BUNDLE_HINT_TEXT, hintText);
+	}
+	
+	@Override
+	public void onCancel(DialogInterface dialog) {
+		super.onCancel(dialog);
+		listener.onTextInputDialogCancelled(dialogId);
+	}
+}