Hedgeroid: Major overhaul of the network connection code. Now it is threaded and
authorMedo <smaxein@googlemail.com>
Thu, 26 Jul 2012 11:01:32 +0200
changeset 7358 57a508884052
parent 7355 5673e95ef647
child 7439 0a494f951dcf
Hedgeroid: Major overhaul of the network connection code. Now it is threaded and errors are handled properly.
project_files/Android-build/SDL-android-project/AndroidManifest.xml
project_files/Android-build/SDL-android-project/res/values/strings.xml
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/MainActivity.java
project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/JnaFrontlib.java
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/LobbyChatFragment.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/NetplayService.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/Room.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
--- a/project_files/Android-build/SDL-android-project/AndroidManifest.xml	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/AndroidManifest.xml	Thu Jul 26 11:01:32 2012 +0200
@@ -46,7 +46,6 @@
             android:theme="@android:style/Theme.NoTitleBar.Fullscreen" />
 
         <service android:name=".Downloader.DownloadService" />
-		<service android:name=".netplay.NetplayService" />
 		
         <activity
             android:name="StartGameActivity"
--- a/project_files/Android-build/SDL-android-project/res/values/strings.xml	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/res/values/strings.xml	Thu Jul 26 11:01:32 2012 +0200
@@ -12,7 +12,7 @@
     <string name="sdcard_not_mounted">There\'s been an error when accessing the SDcard. Please check if there is an SDcard present in the device (internal or external) and if the SDcard is not mounted (via usb to your computer for example). Hedgewars for Android will now quit</string>
 
     <!-- Notification -->
-    <string name="notification_title">Downloading hedgewars files...</string>
+    <string name="notification_title">Downloading hedgewars files&#8230;</string>
     <string name="notification_done">Successfully downloaded: </string>
 
     <!-- Download Activity -->
@@ -22,7 +22,7 @@
     <string name="download_back">Back to main menu</string>
     <string name="download_tryagain">Try again</string>
     <string name="download_failed">The download has failed because of: </string>
-    <string name="download_userexplain">Before starting the game we must download some extra files...</string>
+    <string name="download_userexplain">Before starting the game we must download some extra files&#8230;</string>
     <string name="download_areyousure">Are you sure you want to download this package?</string>
     <string name="download_alreadydownloaded">You\'ve already downloaded this package, are you sure you want to download it again?</string>
     <string name="download_downloadnow">Download now!</string>
@@ -90,7 +90,7 @@
     <!-- Start netgame dialog -->
     <string name="start_netgame_dialog_title">Connect</string>
     <string name="start_netgame_dialog_message">Please select a username.</string>
-    <string name="start_netgame_dialog_playername_hint">Enter a username here</string>
+    <string name="start_netgame_dialog_playername_hint">Username</string>
     
     <string name="lobby_playerlist_contextmenu_info">Info (shown in chat)</string>
     <string name="lobby_playerlist_contextmenu_follow">Follow</string>
@@ -105,6 +105,13 @@
     <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.</string>
+    <string name="error_connection_lost">The connection to the server was lost: %1$s</string>
+    
+    <!-- Dialogs -->
+    <string name="dialog_connecting_title">Please wait</string>
+    <string name="dialog_connecting_message">Connecting to the server&#8230;</string>
+    <string name="dialog_password_title">Password required</string>
+    <string name="dialog_password_message">The server has requested a password to connect as "%1$s".</string>
+    <string name="dialog_password_hint">Password</string>
     
 </resources>
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/MainActivity.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/MainActivity.java	Thu Jul 26 11:01:32 2012 +0200
@@ -25,7 +25,8 @@
 import org.hedgewars.hedgeroid.Downloader.DownloadAssets;
 import org.hedgewars.hedgeroid.Downloader.DownloadListActivity;
 import org.hedgewars.hedgeroid.netplay.LobbyActivity;
-import org.hedgewars.hedgeroid.netplay.NetplayService;
+import org.hedgewars.hedgeroid.netplay.Netplay;
+import org.hedgewars.hedgeroid.netplay.Netplay.State;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -33,6 +34,7 @@
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.SharedPreferences;
@@ -40,7 +42,8 @@
 import android.os.Bundle;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.content.LocalBroadcastManager;
-import android.util.Log;
+import android.text.InputType;
+import android.text.method.PasswordTransformationMethod;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
@@ -50,16 +53,25 @@
 public class MainActivity extends FragmentActivity {
 	private static final int DIALOG_NO_SDCARD = 0;
 	private static final int DIALOG_START_NETGAME = 1;
+	private static final int DIALOG_CONNECTING = 2;
+	private static final int DIALOG_PASSWORD = 3;
 	
 	private static final String PREF_PLAYERNAME = "playerName";
 	
+	private LocalBroadcastManager broadcastManager;
+	
 	private Button downloader, startGame;
 	private ProgressDialog assetsDialog;
+	private String passwordedUsername; // TODO ugly - move dialogs to fragments to get rid of e.g. this
 
 	public void onCreate(Bundle sis){
 		super.onCreate(sis);
+		if(sis != null) {
+			passwordedUsername = sis.getString(PREF_PLAYERNAME);
+		}
 		setContentView(R.layout.main);
 
+		broadcastManager = LocalBroadcastManager.getInstance(getApplicationContext());
 		downloader = (Button)findViewById(R.id.downloader);
 		startGame = (Button)findViewById(R.id.startGame);
 		Button joinLobby = (Button)findViewById(R.id.joinLobby);
@@ -68,10 +80,17 @@
 		startGame.setOnClickListener(startGameClicker);
 		joinLobby.setOnClickListener(new OnClickListener() {
 			public void onClick(View v) {
-				if(!NetplayService.isActive()) {
+				State state = Netplay.getAppInstance(getApplicationContext()).getState();
+				switch(state) {
+				case NOT_CONNECTED:
 					showDialog(DIALOG_START_NETGAME);
-				} else {
+					break;
+				case CONNECTING:
+					startWaitingForConnection();
+					break;
+				default:
 					startActivity(new Intent(getApplicationContext(), LobbyActivity.class));
+					break;
 				}
 			}
 		});
@@ -100,12 +119,44 @@
 		}
 	}
 
+	@Override
+	protected void onSaveInstanceState(Bundle outState) {
+		super.onSaveInstanceState(outState);
+		outState.putString(PREF_PLAYERNAME, passwordedUsername);
+	}
+	
+	@Override
+	protected void onDestroy() {
+		super.onDestroy();
+		stopWaitingForConnection();
+	}
+	
+	@Override
+	protected void onStart() {
+		super.onStart();
+		Netplay.getAppInstance(getApplicationContext()).requestFastTicks();
+	}
+	
+	@Override
+	protected void onStop() {
+		super.onStop();
+		Netplay netplay = Netplay.getAppInstance(getApplicationContext());
+		netplay.unrequestFastTicks();
+		if(netplay.getState() == State.CONNECTING) {
+			netplay.disconnect();
+		}
+	}
+	
 	public Dialog onCreateDialog(int id, Bundle args){
 		switch(id) {
 		case DIALOG_NO_SDCARD:
 			return createNoSdcardDialog();
 		case DIALOG_START_NETGAME:
 			return createStartNetgameDialog();
+		case DIALOG_CONNECTING:
+			return createConnectingDialog();
+		case DIALOG_PASSWORD:
+			return createPasswordDialog();
 		default:
 			throw new IndexOutOfBoundsException();
 		}
@@ -150,11 +201,8 @@
 					edit.putString(PREF_PLAYERNAME, playerName);
 					edit.commit();
 					
-					Intent netplayServiceIntent = new Intent(getApplicationContext(), NetplayService.class);
-					netplayServiceIntent.putExtra(NetplayService.EXTRA_PLAYERNAME, playerName);
-					startService(netplayServiceIntent);
-					
-					LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(connectedReceiver, new IntentFilter(NetplayService.ACTION_CONNECTED));
+					startWaitingForConnection();
+					Netplay.getAppInstance(getApplicationContext()).connectToDefaultServer(playerName);
 				}
 			}
 		});
@@ -162,6 +210,46 @@
 		return builder.create();
 	}
 	
+	private Dialog createConnectingDialog() {
+		ProgressDialog dialog = new ProgressDialog(this);
+		dialog.setOnCancelListener(new OnCancelListener() {
+			public void onCancel(DialogInterface dialog) {
+				Netplay.getAppInstance(getApplicationContext()).disconnect();
+			}
+		});
+		dialog.setIndeterminate(true);
+		dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+		dialog.setTitle(R.string.dialog_connecting_title);
+		dialog.setMessage(getString(R.string.dialog_connecting_message));
+		return dialog;
+	}
+	
+	private Dialog createPasswordDialog() {
+		final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+		final EditText editText = new EditText(this);
+		editText.setHint(R.string.dialog_password_hint);
+		editText.setFreezesText(true);
+		editText.setId(android.R.id.text1);
+		editText.setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
+		editText.setTransformationMethod(PasswordTransformationMethod.getInstance());
+		builder.setView(editText);
+		builder.setTitle(R.string.dialog_password_title);
+		builder.setMessage(getString(R.string.dialog_password_message, passwordedUsername));
+		builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+			public void onClick(DialogInterface dialog, int which) {
+				String password = editText.getText().toString();
+				editText.setText("");
+				Netplay.getAppInstance(getApplicationContext()).sendPassword(password);
+			}
+		});
+		builder.setOnCancelListener(new OnCancelListener() {
+			public void onCancel(DialogInterface dialog) {
+				Netplay.getAppInstance(getApplicationContext()).disconnect();
+			}
+		});
+		return builder.create();
+	}
+	
 	public void onAssetsDownloaded(boolean result){
 		if(!result){
 			Toast.makeText(this, R.string.download_failed, Toast.LENGTH_LONG).show();
@@ -181,10 +269,44 @@
 		}
 	};
 	
+	private void startWaitingForConnection() {
+		broadcastManager.registerReceiver(connectedReceiver, new IntentFilter(Netplay.ACTION_CONNECTED));
+		broadcastManager.registerReceiver(connectionFailedReceiver, new IntentFilter(Netplay.ACTION_DISCONNECTED));
+		broadcastManager.registerReceiver(passwordRequestedReceiver, new IntentFilter(Netplay.ACTION_PASSWORD_REQUESTED));
+		showDialog(DIALOG_CONNECTING);
+	}
+	
+	private void stopWaitingForConnection() {
+		broadcastManager.unregisterReceiver(connectedReceiver);
+		broadcastManager.unregisterReceiver(connectionFailedReceiver);
+		broadcastManager.unregisterReceiver(passwordRequestedReceiver);
+	}
+	
 	private BroadcastReceiver connectedReceiver = new BroadcastReceiver() {
 		@Override
 		public void onReceive(Context context, Intent intent) {
+			stopWaitingForConnection();
+			dismissDialog(DIALOG_CONNECTING);
 			startActivity(new Intent(getApplicationContext(), LobbyActivity.class));
 		}
 	};
+	
+	private BroadcastReceiver connectionFailedReceiver = new BroadcastReceiver() {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			stopWaitingForConnection();
+			dismissDialog(DIALOG_CONNECTING);
+			if(intent.getBooleanExtra(Netplay.EXTRA_HAS_ERROR, true)) {
+				Toast.makeText(getApplicationContext(), intent.getStringExtra(Netplay.EXTRA_MESSAGE), Toast.LENGTH_LONG).show();
+			}
+		}
+	};
+	
+	private BroadcastReceiver passwordRequestedReceiver = new BroadcastReceiver() {
+		@Override
+		public void onReceive(Context context, Intent intent) {
+			passwordedUsername = intent.getStringExtra(Netplay.EXTRA_PLAYERNAME);
+			showDialog(DIALOG_PASSWORD);
+		}
+	};
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/JnaFrontlib.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/JnaFrontlib.java	Thu Jul 26 11:01:32 2012 +0200
@@ -82,17 +82,17 @@
 		/**
 		 * Returns the (native-owned) rooms in this list
 		 */
-		public RoomPtr[] getRooms(int count) {
+		public Room[] getRooms(int count) {
 			Pointer ptr = getPointer();
 			if(ptr == null) {
-				return new RoomPtr[0];
+				return new Room[0];
 			}
 			Pointer[] untypedPtrs = ptr.getPointerArray(0, count);
-			RoomPtr[] typedPtrs = new RoomPtr[count];
+			Room[] result = new Room[count];
 			for(int i=0; i<count; i++) {
-				typedPtrs[i] = new RoomPtr(untypedPtrs[i]);
+				result[i] = RoomPtr.deref(untypedPtrs[i]);
 			}
-			return typedPtrs;
+			return result;
 		}
 	}
 	
@@ -101,9 +101,13 @@
 		public RoomPtr(Pointer ptr) { super(ptr); }
 		
 		public Room deref() {
-			Room result = new Room(getPointer());
-			result.read();
-			return result;
+			return deref(getPointer());
+		}
+		
+		public static Room deref(Pointer p) {
+			RoomStruct r = new RoomStruct(p);
+			r.read();
+			return new Room(r.name, r.map, r.scheme, r.weapons, r.owner, r.playerCount, r.teamCount, r.inProgress);
 		}
 	}
 	
@@ -113,13 +117,13 @@
 	static class SchemePtr extends PointerType { }
 	static class GameSetupPtr extends PointerType { }
 	
-	static class Room extends Structure {
-		public static class byVal extends Room implements Structure.ByValue {}
-		public static class byRef extends Room implements Structure.ByReference {}
+	static class RoomStruct extends Structure {
+		public static class byVal extends RoomStruct implements Structure.ByValue {}
+		public static class byRef extends RoomStruct implements Structure.ByReference {}
 		private static String[] FIELD_ORDER = new String[] {"inProgress", "name", "playerCount", "teamCount", "owner", "map", "scheme", "weapons"};
 		
-		public Room() { super(); setFieldOrder(FIELD_ORDER); }
-		public Room(Pointer ptr) { super(ptr); setFieldOrder(FIELD_ORDER); }
+		public RoomStruct() { super(); setFieldOrder(FIELD_ORDER); }
+		public RoomStruct(Pointer ptr) { super(ptr); setFieldOrder(FIELD_ORDER); }
 		
 	    public boolean inProgress;
 	    public String name;
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyActivity.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyActivity.java	Thu Jul 26 11:01:32 2012 +0200
@@ -1,17 +1,13 @@
 package org.hedgewars.hedgeroid.netplay;
 
 import org.hedgewars.hedgeroid.R;
-import org.hedgewars.hedgeroid.netplay.NetplayService.NetplayBinder;
 
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.ServiceConnection;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.content.LocalBroadcastManager;
 import android.view.LayoutInflater;
@@ -26,13 +22,16 @@
 
 public class LobbyActivity extends FragmentActivity {
     private TabHost tabHost;
-    private NetplayService service;
+    private Netplay netconn;
+    private boolean isInForeground;
     
     private final BroadcastReceiver disconnectReceiver = new BroadcastReceiver() {
 		@Override
 		public void onReceive(Context context, Intent intent) {
-			String message = intent.getStringExtra(NetplayService.EXTRA_MESSAGE);
-			Toast.makeText(getApplicationContext(), "Disconnected: "+message, Toast.LENGTH_LONG).show();
+			if(isInForeground && intent.getBooleanExtra(Netplay.EXTRA_HAS_ERROR, true)) {
+				String message = intent.getStringExtra(Netplay.EXTRA_MESSAGE);
+				Toast.makeText(getApplicationContext(), "Disconnected: "+message, Toast.LENGTH_LONG).show();
+			}
 			finish();
 		}
 	};
@@ -40,9 +39,9 @@
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
-        LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(disconnectReceiver, new IntentFilter(NetplayService.ACTION_DISCONNECTED));
-		bindService(new Intent(this, NetplayService.class), serviceConnection, 0);
-
+        LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(disconnectReceiver, new IntentFilter(Netplay.ACTION_DISCONNECTED));
+        netconn = Netplay.getAppInstance(getApplicationContext());
+        
         setContentView(R.layout.activity_lobby);
         tabHost = (TabHost)findViewById(android.R.id.tabhost);
         if(tabHost != null) {
@@ -63,7 +62,6 @@
     protected void onDestroy() {
     	super.onDestroy();
     	LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(disconnectReceiver);
-    	unbindService(serviceConnection);
     }
     
     private View createIndicatorView(TabHost tabHost, CharSequence label, Drawable icon) {
@@ -84,6 +82,19 @@
         return tabIndicator;
     }
 
+    @Override
+    protected void onStart() {
+    	super.onStart();
+    	isInForeground = true;
+    	Netplay.getAppInstance(getApplicationContext()).requestFastTicks();
+    }
+    
+    @Override
+    protected void onStop() {
+    	super.onStop();
+    	isInForeground = false;
+    	Netplay.getAppInstance(getApplicationContext()).unrequestFastTicks();
+    }
     
 	@Override
 	public boolean onCreateOptionsMenu(Menu menu) {
@@ -99,14 +110,18 @@
 			Toast.makeText(this, R.string.not_implemented_yet, Toast.LENGTH_SHORT).show();
 			return true;
 		case R.id.disconnect:
-			if(service != null && service.isConnected()) {
-				service.disconnect();
-			}
+			netconn.disconnect();
 			return true;
 		default:
 			return super.onOptionsItemSelected(item);
 		}
 	}
+	
+	@Override
+	public void onBackPressed() {
+		netconn.disconnect();
+		super.onBackPressed();
+	}
     
 	/*@Override
 	protected void onCreate(Bundle arg0) {
@@ -146,14 +161,4 @@
         	icicle.putString("currentTab", tabHost.getCurrentTabTag());
         }
     }
-    
-    private ServiceConnection serviceConnection = new ServiceConnection() {
-        public void onServiceConnected(ComponentName className, IBinder binder) {
-        	service = ((NetplayBinder) binder).getService();
-        }
-
-        public void onServiceDisconnected(ComponentName className) {
-        	service = null;
-        }
-    };
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyChatFragment.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyChatFragment.java	Thu Jul 26 11:01:32 2012 +0200
@@ -2,16 +2,9 @@
 
 
 import org.hedgewars.hedgeroid.R;
-import org.hedgewars.hedgeroid.netplay.NetplayService.NetplayBinder;
 
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.support.v4.app.Fragment;
-import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -23,54 +16,36 @@
 import android.widget.TextView.OnEditorActionListener;
 
 public class LobbyChatFragment extends Fragment {
-	private EditText editText;
-	private ListView listView;
 	private ChatlogAdapter adapter;
-	private NetplayService service;
-	
-	private void commitText() {
-		String text = editText.getText().toString();
-		if(service != null && service.isConnected() && text.length()>0) {
-			editText.setText("");
-			service.sendChat(text);
-		}
-	}
+	private Netplay netconn;
 	
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
+		netconn = Netplay.getAppInstance(getActivity().getApplicationContext());
 		adapter = new ChatlogAdapter(getActivity());
+    	adapter.setLog(netconn.lobbyChatlog.getLog());
+    	netconn.lobbyChatlog.registerObserver(adapter);
 	}
 	
 	@Override
 	public void onStart() {
 		super.onStart();
-		getActivity().bindService(new Intent(getActivity(), NetplayService.class), serviceConnection,
-	            Context.BIND_AUTO_CREATE);
 	}
 	
 	@Override
 	public View onCreateView(LayoutInflater inflater, ViewGroup container,
 			Bundle savedInstanceState) {
 		View view = inflater.inflate(R.layout.lobby_chat_fragment, container, false);
-		editText = (EditText) view.findViewById(R.id.lobbyChatInput);
-		listView = (ListView) view.findViewById(R.id.lobbyConsole);
 		
+		ListView listView = (ListView) view.findViewById(R.id.lobbyConsole);
 		listView.setAdapter(adapter);
 		listView.setDivider(null);
 		listView.setDividerHeight(0);
 		listView.setVerticalFadingEdgeEnabled(true);
 		
-        editText.setOnEditorActionListener(new OnEditorActionListener() {
-			public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-				boolean handled = false;
-				if(actionId == EditorInfo.IME_ACTION_SEND) {
-					commitText();
-					handled = true;
-				}
-				return handled;
-			}
-		});
+		EditText editText = (EditText) view.findViewById(R.id.lobbyChatInput);
+        editText.setOnEditorActionListener(new ChatSendListener());
         
 		return view;
 	}
@@ -78,21 +53,21 @@
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
-		getActivity().unbindService(serviceConnection);
+		netconn.lobbyChatlog.unregisterObserver(adapter);
 	}
 
-    private ServiceConnection serviceConnection = new ServiceConnection() {
-        public void onServiceConnected(ComponentName className, IBinder binder) {
-        	Log.d("LobbyChatFragment", "netconn received");
-        	service = ((NetplayBinder) binder).getService();
-        	adapter.setLog(service.lobbyChatlog.getLog());
-        	service.lobbyChatlog.registerObserver(adapter);
-        }
-
-        public void onServiceDisconnected(ComponentName className) {
-        	// TODO navigate away
-        	service.lobbyChatlog.unregisterObserver(adapter);
-        	service = null;
-        }
-    };
+	private final class ChatSendListener implements OnEditorActionListener {
+		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+			boolean handled = false;
+			if(actionId == EditorInfo.IME_ACTION_SEND) {
+				String text = v.getText().toString();
+				if(text.length()>0) {
+					v.setText("");
+					netconn.sendChat(text);
+					handled = true;
+				}
+			}
+			return handled;
+		}
+	}
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Netplay.java	Thu Jul 26 11:01:32 2012 +0200
@@ -0,0 +1,527 @@
+package org.hedgewars.hedgeroid.netplay;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+import org.hedgewars.hedgeroid.R;
+import org.hedgewars.hedgeroid.Utils;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.IntStrCallback;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.MetaschemePtr;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.NetconnPtr;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomArrayPtr;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomCallback;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomListCallback;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomPtr;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.StrCallback;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.StrRoomCallback;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.StrStrCallback;
+import org.hedgewars.hedgeroid.netplay.JnaFrontlib.VoidCallback;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+import android.util.Pair;
+
+import com.sun.jna.Pointer;
+
+/**
+ * This class manages the application's networking state.
+ */
+public class Netplay {
+	public static enum State { NOT_CONNECTED, CONNECTING, LOBBY, ROOM, INGAME }
+	
+	// Extras in broadcasts
+	public static final String EXTRA_PLAYERNAME = "playerName";
+	public static final String EXTRA_MESSAGE = "message";
+	public static final String EXTRA_HAS_ERROR = "hasError";
+	
+	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 DEFAULT_SERVER = "netserver.hedgewars.org";
+	public static final int DEFAULT_PORT = 46631;
+		
+	private final Context appContext;
+	private final LocalBroadcastManager broadcastManager;
+	private final FromNetHandler fromNetHandler = new FromNetHandler();
+	
+	private State state;
+	private int foregroundUsers = 0;
+	
+	// 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 MessageLog lobbyChatlog;
+	public final MessageLog roomChatlog;
+	
+	public Netplay(Context appContext) {
+		this.appContext = appContext;
+		broadcastManager = LocalBroadcastManager.getInstance(appContext);
+		lobbyChatlog = new MessageLog(appContext);
+		roomChatlog = new MessageLog(appContext);
+		state = State.NOT_CONNECTED;
+	}
+	
+	private void clearState() {
+		playerList.clear();
+		roomList.clear();
+		lobbyChatlog.clear();
+		roomChatlog.clear();
+	}
+	
+	public void connectToDefaultServer(String playerName) {
+		connect(playerName, DEFAULT_SERVER, DEFAULT_PORT);
+	}
+	
+	/**
+	 * Establish a new connection. Only call if the current state is NOT_CONNECTED.
+	 * 
+	 * The state will switch to CONNECTING immediately. After that, it can asynchronously change to any other state.
+	 * State changes are indicated by broadcasts. In particular, if an error occurs while trying to connect, the state
+	 * will change back to NOT_CONNECTED and an ACTION_DISCONNECTED broadcast is sent.
+	 */
+	public void connect(String name, String host, int port) {
+		if(state != State.NOT_CONNECTED) {
+			throw new IllegalStateException("Attempt to start a new connection while the old one was still running.");
+		}
+		
+		clearState();
+		state = State.CONNECTING;
+		connection = ThreadedNetConnection.startConnection(appContext, fromNetHandler, name, host, port);
+		connection.setFastTickRate(foregroundUsers > 0);
+	}
+	
+	public void sendNick(String nick) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_NICK, nick); }
+	public void sendPassword(String password) { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_SEND_PASSWORD, password); }
+	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 disconnect() { sendToNet(ThreadedNetConnection.ToNetHandler.MSG_DISCONNECT, "User Quit"); }
+	
+	private static Netplay instance;
+	
+	/**
+	 * Retrieve the single app-wide instance of the netplay interface, creating it if it
+	 * does not exist yet.
+	 * 
+	 * @param applicationContext
+	 * @return
+	 */
+	public static Netplay getAppInstance(Context applicationContext) {
+		if(instance == null) {
+			// We'll just do it here and never quit it again...
+			if(Flib.INSTANCE.flib_init() != 0) {
+				throw new RuntimeException("Unable to start frontlib");
+			}
+			instance = new Netplay(applicationContext);
+		}
+		return instance;
+	}
+
+	public State getState() {
+		return state;
+	}
+	
+	/**
+	 * 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
+	 * to conserve battery power.
+	 * Once you no longer need fast updates, call unrequestFastTicks.
+	 */
+	public void requestFastTicks() {
+		if(foregroundUsers == Integer.MAX_VALUE) {
+			throw new RuntimeException("Reference counter overflow");
+		}
+		if(foregroundUsers == 0 && connection != null) {
+			connection.setFastTickRate(true);
+		}
+		foregroundUsers++;
+	}
+	
+	public void unrequestFastTicks() {
+		if(foregroundUsers == 0) {
+			throw new RuntimeException("Reference counter underflow");
+		}
+		foregroundUsers--;
+		if(foregroundUsers == 0 && connection != null) {
+			connection.setFastTickRate(false);
+		}
+	}
+	
+	private boolean sendToNet(int what) {
+		if(connection != null) {
+			Handler handler = connection.toNetHandler;
+			return handler.sendMessage(handler.obtainMessage(what));
+		} else {
+			return false;
+		}
+	}
+	
+	private boolean sendToNet(int what, Object obj) {
+		if(connection != null) {
+			Handler handler = connection.toNetHandler;
+			return handler.sendMessage(handler.obtainMessage(what, obj));
+		} else {
+			return false;
+		}
+	}
+	
+	private MessageLog getCurrentLog() {
+		if(state == State.ROOM || state == State.INGAME) {
+			return roomChatlog;
+		} else {
+			return lobbyChatlog;
+		}
+	}
+	
+	/**
+	 * Processes messages from the networking system. Always runs on the main thread.
+	 */
+	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 FromNetHandler() {
+			super(Looper.getMainLooper());
+		}
+		
+		@SuppressWarnings("unchecked")
+		@Override
+		public void handleMessage(Message msg) {
+			switch(msg.what) {
+			case MSG_LOBBY_JOIN: {
+				String name = (String)msg.obj;
+				playerList.addPlayerWithNewId(name);
+				lobbyChatlog.appendPlayerJoin(name);
+				break;
+			}
+			case MSG_LOBBY_LEAVE: {
+				Pair<String, String> args = (Pair<String, String>)msg.obj;
+				playerList.removePlayer(args.first);
+				lobbyChatlog.appendPlayerLeave(args.first, args.second);
+				break;
+			}
+			case MSG_CHAT: {
+				Pair<String, String> args = (Pair<String, String>)msg.obj;
+				getCurrentLog().appendChat(args.first, args.second);
+				break;
+			}
+			case MSG_MESSAGE: {
+				getCurrentLog().appendMessage(msg.arg1, (String)msg.obj);
+				break;
+			}
+			case MSG_ROOM_ADD: {
+				roomList.addRoomWithNewId((Room)msg.obj);
+				break;
+			}
+			case MSG_ROOM_UPDATE: {
+				Pair<String, Room> args = (Pair<String, Room>)msg.obj;
+				roomList.updateRoom(args.first, args.second);
+				break;
+			}
+			case MSG_ROOM_DELETE: {
+				roomList.removeRoom((String)msg.obj);
+				break;
+			}
+			case MSG_ROOMLIST: {
+				roomList.updateList((Room[])msg.obj);
+				break;
+			}
+			case MSG_CONNECTED: {
+				state = 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;
+				connection = null;
+				Intent intent = new Intent(ACTION_DISCONNECTED);
+				intent.putExtra(EXTRA_HAS_ERROR, args.first);
+				intent.putExtra(EXTRA_MESSAGE, args.second);
+				broadcastManager.sendBroadcastSync(intent);
+				break;
+			}
+			case MSG_PASSWORD_REQUEST: {
+				Intent intent = new Intent(ACTION_PASSWORD_REQUESTED);
+				intent.putExtra(EXTRA_PLAYERNAME, (String)msg.obj);
+				broadcastManager.sendBroadcast(intent);
+				break;
+			}
+			default: {
+				Log.e("FromNetHandler", "Unknown message type: "+msg.what);
+				break;
+			}
+			}
+		}
+	}
+	
+	/**
+	 * This class handles the actual communication with the networking library, on a separate thread.
+	 */
+	private static class ThreadedNetConnection {
+		private static final long TICK_INTERVAL_FAST = 100;
+		private static final long TICK_INTERVAL_SLOW = 5000;
+		private static final JnaFrontlib FLIB = Flib.INSTANCE;
+		
+		public final ToNetHandler toNetHandler;
+		
+		private final Context appContext;
+		private final FromNetHandler fromNetHandler;
+		private final TickHandler tickHandler;
+		
+		/**
+		 * conn can only be null while connecting (the first thing in the thread), and directly after disconnecting,
+		 * in the same message (the looper is shut down on disconnect, so there will be no messages after that).
+		 */
+		private NetconnPtr conn;
+		private String playerName;
+		
+		private ThreadedNetConnection(Context appContext, FromNetHandler fromNetHandler) {
+			this.appContext = appContext;
+			this.fromNetHandler = fromNetHandler;
+			
+			HandlerThread thread = new HandlerThread("NetThread");
+			thread.start();
+			toNetHandler = new ToNetHandler(thread.getLooper());
+			tickHandler = new TickHandler(thread.getLooper(), TICK_INTERVAL_FAST, tickCb);
+		}
+		
+		private void connect(final String name, final String host, final int port) {
+			toNetHandler.post(new Runnable() {
+				public void run() {
+					playerName = name == null ? "Player" : name;
+					MetaschemePtr meta = null;
+					File dataPath;
+					try {
+						dataPath = Utils.getDataPathFile(appContext);
+					} catch (FileNotFoundException e) {
+						shutdown(true, appContext.getString(R.string.sdcard_not_mounted));
+						return;
+					}
+					String metaschemePath = new File(dataPath, "metasettings.ini").getAbsolutePath();
+					meta = FLIB.flib_metascheme_from_ini(metaschemePath);
+					if(meta == null) {
+						shutdown(true, appContext.getString(R.string.error_unexpected, "Missing metasettings.ini"));
+						return;
+					}
+					conn = FLIB.flib_netconn_create(playerName, meta, dataPath.getAbsolutePath(), host, port);
+					if(conn == null) {
+						shutdown(true, appContext.getString(R.string.error_connection_failed));
+						return;
+					}
+					FLIB.flib_netconn_onLobbyJoin(conn, lobbyJoinCb, null);
+					FLIB.flib_netconn_onLobbyLeave(conn, lobbyLeaveCb, null);
+					FLIB.flib_netconn_onChat(conn, chatCb, null);
+					FLIB.flib_netconn_onMessage(conn, messageCb, null);
+					FLIB.flib_netconn_onRoomAdd(conn, roomAddCb, null);
+					FLIB.flib_netconn_onRoomUpdate(conn, roomUpdateCb, null);
+					FLIB.flib_netconn_onRoomDelete(conn, roomDeleteCb, null);
+					FLIB.flib_netconn_onConnected(conn, connectedCb, null);
+					FLIB.flib_netconn_onRoomlist(conn, roomlistCb, null);
+					FLIB.flib_netconn_onDisconnected(conn, disconnectCb, null);
+					FLIB.flib_netconn_onPasswordRequest(conn, passwordRequestCb, null);
+					FLIB.flib_metascheme_release(meta);
+					tickHandler.start();
+				}
+			});
+		}
+		
+		public static ThreadedNetConnection startConnection(Context appContext, FromNetHandler fromNetHandler, String playerName, String host, int port) {
+			ThreadedNetConnection result = new ThreadedNetConnection(appContext, fromNetHandler);
+			result.connect(playerName, host, port);
+			return result;
+		}
+		
+		public void setFastTickRate(boolean fastTickRate) {
+			tickHandler.setInterval(fastTickRate ? TICK_INTERVAL_FAST : TICK_INTERVAL_SLOW);
+		}
+		
+		private final Runnable tickCb = new Runnable() {
+			public void run() {
+				FLIB.flib_netconn_tick(conn);
+			}
+		};
+		
+		private final StrCallback lobbyJoinCb = new StrCallback() {
+			public void callback(Pointer context, String name) {
+				sendFromNet(FromNetHandler.MSG_LOBBY_JOIN, name);
+			}
+		};
+		
+		private final StrStrCallback lobbyLeaveCb = new StrStrCallback() {
+			public void callback(Pointer context, String name, String msg) {
+				sendFromNet(FromNetHandler.MSG_LOBBY_LEAVE, Pair.create(name, msg));
+			}
+		};
+		
+		private final StrStrCallback chatCb = new StrStrCallback() {
+			public void callback(Pointer context, String name, String msg) {
+				sendFromNet(FromNetHandler.MSG_CHAT, Pair.create(name, msg));
+			}
+		};
+		
+		private final IntStrCallback messageCb = new IntStrCallback() {
+			public void callback(Pointer context, int type, String msg) {
+				sendFromNet(FromNetHandler.MSG_MESSAGE, type, msg);
+			}
+		};
+		
+		private final RoomCallback roomAddCb = new RoomCallback() {
+			public void callback(Pointer context, RoomPtr roomPtr) {
+				sendFromNet(FromNetHandler.MSG_ROOM_ADD, roomPtr.deref());
+			}
+		};
+		
+		private final StrRoomCallback roomUpdateCb = new StrRoomCallback() {
+			public void callback(Pointer context, String name, RoomPtr roomPtr) {
+				sendFromNet(FromNetHandler.MSG_ROOM_UPDATE, Pair.create(name, roomPtr.deref()));
+			}
+		};
+		
+		private final StrCallback roomDeleteCb = new StrCallback() {
+			public void callback(Pointer context, final String name) {
+				sendFromNet(FromNetHandler.MSG_ROOM_DELETE, name);
+			}
+		};
+		
+		private final RoomListCallback roomlistCb = new RoomListCallback() {
+			public void callback(Pointer context, RoomArrayPtr arg1, int count) {
+				sendFromNet(FromNetHandler.MSG_ROOMLIST, arg1.getRooms(count));
+			}
+		};
+		
+		private final VoidCallback connectedCb = new VoidCallback() {
+			public void callback(Pointer context) {
+				FLIB.flib_netconn_send_request_roomlist(conn);
+				playerName = FLIB.flib_netconn_get_playername(conn);
+				sendFromNet(FromNetHandler.MSG_CONNECTED, playerName);
+			}
+		};
+		
+		private final StrCallback passwordRequestCb = new StrCallback() {
+			public void callback(Pointer context, String nickname) {
+				sendFromNet(FromNetHandler.MSG_PASSWORD_REQUEST, playerName);
+			}
+		};
+		
+		private void shutdown(boolean error, String message) {
+			if(conn != null) {
+				FLIB.flib_netconn_destroy(conn);
+				conn = null;
+			}
+			tickHandler.stop();
+			toNetHandler.getLooper().quit();
+			sendFromNet(FromNetHandler.MSG_DISCONNECTED, Pair.create(error, message));
+		}
+		
+		private final IntStrCallback disconnectCb = new IntStrCallback() {
+			public void callback(Pointer context, int reason, String message) {
+				Boolean error = reason != JnaFrontlib.NETCONN_DISCONNECT_NORMAL;
+				String messageForUser = createDisconnectUserMessage(appContext.getResources(), reason, message);
+				shutdown(error, messageForUser);
+			}
+		};
+		
+		private static String createDisconnectUserMessage(Resources res, int reason, String message) {
+			switch(reason) {
+			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);
+			case JnaFrontlib.NETCONN_DISCONNECT_INTERNAL_ERROR:
+				return res.getString(R.string.error_unexpected, message);
+			case JnaFrontlib.NETCONN_DISCONNECT_SERVER_TOO_OLD:
+				return res.getString(R.string.error_server_too_old);
+			default:
+				return message;
+			}
+		}
+		
+		private boolean sendFromNet(int what, Object obj) {
+			return fromNetHandler.sendMessage(fromNetHandler.obtainMessage(what, obj));
+		}
+		
+		private boolean sendFromNet(int what, int arg1, Object obj) {
+			return fromNetHandler.sendMessage(fromNetHandler.obtainMessage(what, arg1, 0, obj));
+		}
+		
+		/**
+		 * Processes messages to the networking system. Runs on a non-main thread.
+		 */
+		public final class ToNetHandler extends Handler {
+			public static final int MSG_SEND_NICK = 0;
+			public static final int MSG_SEND_PASSWORD = 1;
+			public static final int MSG_SEND_QUIT = 2;
+			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 ToNetHandler(Looper looper) {
+				super(looper);
+			}
+			
+			@Override
+			public void handleMessage(Message msg) {
+				switch(msg.what) {
+				case MSG_SEND_NICK: {
+					FLIB.flib_netconn_send_nick(conn, (String)msg.obj);
+					break;
+				}
+				case MSG_SEND_PASSWORD: {
+					FLIB.flib_netconn_send_password(conn, (String)msg.obj);
+					break;
+				}
+				case MSG_SEND_QUIT: {
+					FLIB.flib_netconn_send_quit(conn, (String)msg.obj);
+					break;
+				}
+				case MSG_SEND_ROOMLIST_REQUEST: {
+					FLIB.flib_netconn_send_request_roomlist(conn); // TODO restrict to lobby state?
+					break;
+				}
+				case MSG_SEND_PLAYER_INFO_REQUEST: {
+					FLIB.flib_netconn_send_playerInfo(conn, (String)msg.obj);
+					break;
+				}
+				case MSG_SEND_CHAT: {
+					if(FLIB.flib_netconn_send_chat(conn, (String)msg.obj) == 0) {
+						sendFromNet(FromNetHandler.MSG_CHAT, Pair.create(playerName, (String)msg.obj));
+					}
+					break;
+				}
+				case MSG_DISCONNECT: {
+					FLIB.flib_netconn_send_quit(conn, (String)msg.obj);
+					shutdown(false, "User quit");
+					break;
+				}
+				default: {
+					Log.e("ToNetHandler", "Unknown message type: "+msg.what);
+					break;
+				}
+				}
+			}
+		}
+	}
+}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/NetplayService.java	Tue Jul 24 16:57:48 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,317 +0,0 @@
-package org.hedgewars.hedgeroid.netplay;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-
-import org.hedgewars.hedgeroid.R;
-import org.hedgewars.hedgeroid.Utils;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.IntStrCallback;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.MetaschemePtr;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.NetconnPtr;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomArrayPtr;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomCallback;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomListCallback;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomPtr;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.StrCallback;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.StrRoomCallback;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.StrStrCallback;
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.VoidCallback;
-
-import com.sun.jna.Pointer;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.IBinder;
-import android.support.v4.content.LocalBroadcastManager;
-import android.util.Log;
-
-public class NetplayService extends Service {
-	// Parameter extras for starting the service
-	public static final String EXTRA_PLAYERNAME = "playername";
-	public static final String EXTRA_PORT = "port";
-	public static final String EXTRA_HOST = "host";
-	
-	// Extras in broadcasts
-	public static final String EXTRA_MESSAGE = "message";
-	public static final String EXTRA_HAS_ERROR = "hasError";
-	
-	
-	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";
-	
-	private static final JnaFrontlib FLIB = Flib.INSTANCE;
-	public static final String DEFAULT_SERVER = "netserver.hedgewars.org";
-	public static final int DEFAULT_PORT = 46631;
-	
-	private static final long TICK_INTERVAL_MS_BOUND = 100;
-	private static final long TICK_INTERVAL_MS_UNBOUND = 2000;
-	
-	// null if the service is not active. Only updated from the main thread.
-	public static NetplayService instance;
-	
-	private final NetplayBinder binder = new NetplayBinder();
-	private TickHandler tickHandler;
-	private LocalBroadcastManager broadcastManager;
-	
-	private String playerName;
-	private NetconnPtr conn;
-	private boolean joined; // True once we have been admitted to the lobby
-	
-	public final PlayerList playerList = new PlayerList();
-	public final RoomList roomList = new RoomList();
-	public MessageLog lobbyChatlog;
-	public MessageLog roomChatlog;
-	
-	@Override
-	public IBinder onBind(Intent intent) {
-		Log.d("NetplayService", "onBind");
-		tickHandler.setInterval(TICK_INTERVAL_MS_BOUND);
-		return binder;
-	}
-	
-	@Override
-	public void onRebind(Intent intent) {
-		Log.d("NetplayService", "onRebind");
-		tickHandler.setInterval(TICK_INTERVAL_MS_BOUND);
-	}
-	
-	@Override
-	public boolean onUnbind(Intent intent) {
-		Log.d("NetplayService", "onUnbind");
-		tickHandler.setInterval(TICK_INTERVAL_MS_UNBOUND);
-		return true;
-	}
-	
-	@Override
-	public int onStartCommand(Intent intent, int flags, int startId) {
-		Log.d("NetplayService", "onStartCommand");
-		if(conn != null) {
-			Log.e("NetplayService", "Attempt to start while running");
-			return START_NOT_STICKY;
-		}
-		joined = false;
-		playerList.clear();
-		roomList.clear();
-		lobbyChatlog.clear();
-		roomChatlog.clear();
-		
-		playerName = intent.getStringExtra(EXTRA_PLAYERNAME);
-		if(playerName == null) playerName = "Player";
-		int port = intent.getIntExtra(EXTRA_PORT, DEFAULT_PORT);
-		String host = intent.getStringExtra(EXTRA_HOST);
-		if(host==null) host = DEFAULT_SERVER;
-		
-		MetaschemePtr meta = null;
-		File dataPath;
-		try {
-			dataPath = Utils.getDataPathFile(getApplicationContext());
-		} catch (FileNotFoundException e) {
-			stopWithError(getString(R.string.sdcard_not_mounted));
-			return START_NOT_STICKY;
-		}
-		String metaschemePath = new File(dataPath, "metasettings.ini").getAbsolutePath();
-		meta = FLIB.flib_metascheme_from_ini(metaschemePath);
-		if(meta == null) {
-			stopWithError(getString(R.string.error_unexpected, "Missing metasettings.ini"));
-			return START_NOT_STICKY;
-		}
-		conn = FLIB.flib_netconn_create(playerName, meta, dataPath.getAbsolutePath(), host, port);
-		if(conn == null) {
-			stopWithError(getString(R.string.error_connection_failed));
-			return START_NOT_STICKY;
-		}
-		FLIB.flib_netconn_onLobbyJoin(conn, lobbyJoinCb, null);
-		FLIB.flib_netconn_onLobbyLeave(conn, lobbyLeaveCb, null);
-		FLIB.flib_netconn_onChat(conn, chatCb, null);
-		FLIB.flib_netconn_onMessage(conn, messageCb, null);
-		FLIB.flib_netconn_onRoomAdd(conn, roomAddCb, null);
-		FLIB.flib_netconn_onRoomUpdate(conn, roomUpdateCb, null);
-		FLIB.flib_netconn_onRoomDelete(conn, roomDeleteCb, null);
-		FLIB.flib_netconn_onConnected(conn, connectedCb, null);
-		FLIB.flib_netconn_onRoomlist(conn, roomlistCb, null);
-		FLIB.flib_netconn_onDisconnected(conn, disconnectCb, null);
-		FLIB.flib_metascheme_release(meta);
-		tickHandler.start();
-		instance = this;
-		return START_NOT_STICKY;
-	}
-	
-	private void stopWithoutError() {
-		Intent intent = new Intent(ACTION_DISCONNECTED);
-		intent.putExtra(EXTRA_HAS_ERROR, false);
-		broadcastManager.sendBroadcast(intent);
-		stopSelf();
-	}
-	
-	private void stopWithError(String userMessage) {
-		Intent intent = new Intent(ACTION_DISCONNECTED);
-		intent.putExtra(EXTRA_MESSAGE, userMessage);
-		intent.putExtra(EXTRA_HAS_ERROR, true);
-		broadcastManager.sendBroadcast(intent);
-		stopSelf();
-	}
-	
-	@Override
-	public void onCreate() {
-		Log.d("NetplayService", "onCreate");
-		broadcastManager = LocalBroadcastManager.getInstance(getApplicationContext());
-		lobbyChatlog = new MessageLog(getApplicationContext());
-		roomChatlog = new MessageLog(getApplicationContext());
-		tickHandler = new TickHandler(getMainLooper(), TICK_INTERVAL_MS_UNBOUND, new Runnable() {
-			public void run() {
-				if(conn != null) {
-					FLIB.flib_netconn_tick(conn);
-				}
-			}
-		});
-		if(Flib.INSTANCE.flib_init() != 0) {
-			stopWithError(getString(R.string.error_unexpected, "Unable to start frontlib"));
-		}
-	}
-	
-	@Override
-	public void onDestroy() {
-		instance = null;
-		Log.d("NetplayService", "onDestroy");
-		tickHandler.stop();
-		if(conn != null) {
-			FLIB.flib_netconn_destroy(conn);
-			conn = null;
-		}
-		Flib.INSTANCE.flib_quit();
-	}
-
-	public class NetplayBinder extends Binder {
-		NetplayService getService() {
-            return NetplayService.this;
-        }
-	}
-	
-	private StrCallback lobbyJoinCb = new StrCallback() {
-		public void callback(Pointer context, String arg1) {
-			playerList.addPlayerWithNewId(arg1);
-			lobbyChatlog.appendPlayerJoin(arg1);
-		}
-	};
-	
-	private StrStrCallback lobbyLeaveCb = new StrStrCallback() {
-		public void callback(Pointer context, String name, String msg) {
-			playerList.removePlayer(name);
-			lobbyChatlog.appendPlayerLeave(name, msg);
-		}
-	};
-	
-	private StrStrCallback chatCb = new StrStrCallback() {
-		public void callback(Pointer context, String name, String msg) {
-			getCurrentLog().appendChat(name, msg);
-		}
-	};
-	
-	private IntStrCallback messageCb = new IntStrCallback() {
-		public void callback(Pointer context, int type, String msg) {
-			getCurrentLog().appendMessage(type, msg);
-		}
-	};
-	
-	private RoomCallback roomAddCb = new RoomCallback() {
-		public void callback(Pointer context, RoomPtr roomPtr) {
-			roomList.addRoomWithNewId(roomPtr);
-		}
-	};
-	
-	private StrRoomCallback roomUpdateCb = new StrRoomCallback() {
-		public void callback(Pointer context, String name, RoomPtr roomPtr) {
-			roomList.updateRoom(name, roomPtr);
-		}
-	};
-	
-	private StrCallback roomDeleteCb = new StrCallback() {
-		public void callback(Pointer context, String name) {
-			roomList.removeRoom(name);
-		}
-	};
-	
-	private VoidCallback connectedCb = new VoidCallback() {
-		public void callback(Pointer context) {
-			broadcastManager.sendBroadcast(new Intent(ACTION_CONNECTED));
-			joined = true;
-			FLIB.flib_netconn_send_request_roomlist(conn);
-		}
-	};
-	
-	private RoomListCallback roomlistCb = new RoomListCallback() {
-		public void callback(Pointer context, RoomArrayPtr arg1, int count) {
-			roomList.updateList(arg1.getRooms(count));
-		}
-	};
-	
-	private IntStrCallback disconnectCb = new IntStrCallback() {
-		public void callback(Pointer context, int reason, String arg2) {
-			switch(reason) {
-			case JnaFrontlib.NETCONN_DISCONNECT_AUTH_FAILED:
-				stopWithError(getString(R.string.error_auth_failed));
-				break;
-			case JnaFrontlib.NETCONN_DISCONNECT_CONNLOST:
-				stopWithError(getString(R.string.error_connection_lost));
-				break;
-			case JnaFrontlib.NETCONN_DISCONNECT_INTERNAL_ERROR:
-				stopWithError(getString(R.string.error_unexpected, arg2));
-				break;
-			case JnaFrontlib.NETCONN_DISCONNECT_NORMAL:
-				stopWithoutError();
-				break;
-			case JnaFrontlib.NETCONN_DISCONNECT_SERVER_TOO_OLD:
-				stopWithError(getString(R.string.error_server_too_old));
-				break;
-			default:
-				stopWithError(arg2);
-				break;
-			}
-			FLIB.flib_netconn_destroy(conn);
-			conn = null;
-		}
-	};
-	
-	public void disconnect() {
-		if(conn != null) {
-			FLIB.flib_netconn_send_quit(conn, "User quit");
-			FLIB.flib_netconn_destroy(conn);
-			conn = null;
-		}
-		stopWithoutError();
-	}
-	
-	public void sendChat(String s) {
-		FLIB.flib_netconn_send_chat(conn, s);
-		if(FLIB.flib_netconn_is_in_room_context(conn)) {
-			roomChatlog.appendChat(playerName, s);
-		} else {
-			lobbyChatlog.appendChat(playerName, s);
-		}
-	}
-	
-	private MessageLog getCurrentLog() {
-		if(FLIB.flib_netconn_is_in_room_context(conn)) {
-			return roomChatlog;
-		} else {
-			return lobbyChatlog;
-		}
-	}
-	
-	public void sendNick(String nick) { FLIB.flib_netconn_send_nick(conn, nick); }
-	public void sendPassword(String password) { FLIB.flib_netconn_send_password(conn, password); }
-	public void sendQuit(String message) { FLIB.flib_netconn_send_quit(conn, message); }
-	public void sendRoomlistRequest() { if(joined) FLIB.flib_netconn_send_request_roomlist(conn); }
-	public void sendPlayerInfoQuery(String name) { FLIB.flib_netconn_send_playerInfo(conn, name); }
-	
-	public boolean isConnected() {
-		return conn != null;
-	}
-
-	public static boolean isActive() {
-		return instance!=null;
-	}
-}
-
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Player.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Player.java	Thu Jul 26 11:01:32 2012 +0200
@@ -1,21 +1,9 @@
 package org.hedgewars.hedgeroid.netplay;
 
-import java.util.Comparator;
-
 public class Player {
-	public static final ByNameComparator NAME_COMPARATOR = new ByNameComparator(); 
-
 	public final String name;
-	public final long id; // for ListView
 	
-	public Player(String name, long id) {
+	public Player(String name) {
 		this.name = name;
-		this.id = id;
-	}
-	
-	private static final class ByNameComparator implements Comparator<Player> {
-		public int compare(Player lhs, Player rhs) {
-			return lhs.name.compareToIgnoreCase(rhs.name);
-		}
 	}
 }
\ No newline at end of file
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerList.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerList.java	Thu Jul 26 11:01:32 2012 +0200
@@ -5,14 +5,14 @@
 import java.util.TreeMap;
 
 import android.database.DataSetObservable;
+import android.util.Pair;
 
 public class PlayerList extends DataSetObservable {
 	private long nextId = 1;
-	private Map<String, Player> players = new TreeMap<String, Player>();
+	private Map<String, Pair<Player, Long>> players = new TreeMap<String, Pair<Player, Long>>();
 	
 	public void addPlayerWithNewId(String name) {
-		Player p = new Player(name, nextId++);
-		players.put(name, p);
+		players.put(name, Pair.create(new Player(name), nextId++));
 		notifyChanged();
 	}
 	
@@ -29,7 +29,7 @@
 		}
 	}
 
-	public Map<String, Player> getMap() {
+	public Map<String, Pair<Player, Long>> getMap() {
 		return Collections.unmodifiableMap(players);
 	}
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerListAdapter.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerListAdapter.java	Thu Jul 26 11:01:32 2012 +0200
@@ -2,12 +2,14 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import org.hedgewars.hedgeroid.R;
 
 import android.content.Context;
 import android.database.DataSetObserver;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -15,7 +17,7 @@
 import android.widget.TextView;
 
 public class PlayerListAdapter extends BaseAdapter {
-	private List<Player> players = new ArrayList<Player>();
+	private List<Pair<Player, Long>> players = new ArrayList<Pair<Player, Long>>();
 	private Context context;
 	private PlayerList playerList;
 	
@@ -40,11 +42,11 @@
 	}
 
 	public Player getItem(int position) {
-		return players.get(position);
+		return players.get(position).first;
 	}
 
 	public long getItemId(int position) {
-		return players.get(position).id;
+		return players.get(position).second;
 	}
 
 	public boolean hasStableIds() {
@@ -61,7 +63,6 @@
 	}
 	
 	public void invalidate() {
-		players = new ArrayList<Player>();
 		if(playerList != null) {
 			playerList.unregisterObserver(observer);
 		}
@@ -70,8 +71,8 @@
 	}
 	
 	private void reloadFromList(PlayerList list) {
-		players = new ArrayList<Player>(list.getMap().values());
-		Collections.sort(players, Player.NAME_COMPARATOR);
+		players = new ArrayList<Pair<Player, Long>>(list.getMap().values());
+		Collections.sort(players, AlphabeticalOrderComparator.INSTANCE);
 		notifyDataSetChanged();
 	}
 	
@@ -85,9 +86,16 @@
 			tv.setCompoundDrawablesWithIntrinsicBounds(R.drawable.human, 0, 0, 0);
 		}
 
-		String player = players.get(position).name;
+		String player = players.get(position).first.name;
 		TextView username = (TextView) v.findViewById(android.R.id.text1);
 		username.setText(player);
 		return v;
 	}
+	
+	private static final class AlphabeticalOrderComparator implements Comparator<Pair<Player, Long>> {
+		public static final AlphabeticalOrderComparator INSTANCE = new AlphabeticalOrderComparator();
+		public int compare(Pair<Player, Long> lhs, Pair<Player, Long> rhs) {
+			return lhs.first.name.compareToIgnoreCase(rhs.first.name);
+		};
+	}
 }
\ No newline at end of file
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerlistFragment.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerlistFragment.java	Thu Jul 26 11:01:32 2012 +0200
@@ -1,37 +1,39 @@
 package org.hedgewars.hedgeroid.netplay;
 
 import org.hedgewars.hedgeroid.R;
-import org.hedgewars.hedgeroid.netplay.NetplayService.NetplayBinder;
 
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.ServiceConnection;
 import android.os.Bundle;
-import android.os.IBinder;
 import android.support.v4.app.ListFragment;
 import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
 import android.view.LayoutInflater;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ContextMenu.ContextMenuInfo;
+import android.widget.AdapterView.AdapterContextMenuInfo;
 import android.widget.Toast;
-import android.widget.AdapterView.AdapterContextMenuInfo;
 
 public class PlayerlistFragment extends ListFragment {
-	private NetplayService netplayService;
+	private Netplay netconn;
 	private PlayerListAdapter playerListAdapter;
 	
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
-		getActivity().bindService(new Intent(getActivity(), NetplayService.class), serviceConnection, 0);
+		netconn = Netplay.getAppInstance(getActivity().getApplicationContext());
 		playerListAdapter = new PlayerListAdapter(getActivity());
+		playerListAdapter.setList(Netplay.getAppInstance(getActivity().getApplicationContext()).playerList);
 		setListAdapter(playerListAdapter);
 	}
 
 	@Override
+	public void onDestroy() {
+		super.onDestroy();
+		playerListAdapter.invalidate();
+	}
+	
+	@Override
 	public void onActivityCreated(Bundle savedInstanceState) {
 		super.onActivityCreated(savedInstanceState);
 		registerForContextMenu(getListView());
@@ -41,8 +43,11 @@
 	public void onCreateContextMenu(ContextMenu menu, View v,
 			ContextMenuInfo menuInfo) {
 		super.onCreateContextMenu(menu, v, menuInfo);
+		AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
 		MenuInflater inflater = getActivity().getMenuInflater();
 		inflater.inflate(R.menu.lobby_playerlist_context, menu);
+		menu.setHeaderIcon(R.drawable.human);
+		menu.setHeaderTitle(playerListAdapter.getItem(info.position).name);
 	}
 	
 	@Override
@@ -51,9 +56,7 @@
 		switch(item.getItemId()) {
 		case R.id.player_info:
 			Player p = playerListAdapter.getItem(info.position);
-			if(netplayService != null) {
-				netplayService.sendPlayerInfoQuery(p.name);
-			}
+			netconn.sendPlayerInfoQuery(p.name);
 			return true;
 		case R.id.player_follow:
 			Toast.makeText(getActivity(), R.string.not_implemented_yet, Toast.LENGTH_SHORT).show();
@@ -64,27 +67,8 @@
 	}
 	
 	@Override
-	public void onDestroy() {
-		super.onDestroy();
-		getActivity().unbindService(serviceConnection);
-	}
-	
-	@Override
 	public View onCreateView(LayoutInflater inflater, ViewGroup container,
 			Bundle savedInstanceState) {
 		return inflater.inflate(R.layout.lobby_players_fragment, container, false);
 	}
-	
-    private ServiceConnection serviceConnection = new ServiceConnection() {
-        public void onServiceConnected(ComponentName className, IBinder binder) {
-        	netplayService = ((NetplayBinder) binder).getService();
-        	playerListAdapter.setList(netplayService.playerList);
-        }
-
-        public void onServiceDisconnected(ComponentName className) {
-        	// TODO navigate away
-        	playerListAdapter.invalidate();
-        	netplayService = null;
-        }
-    };
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Room.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Room.java	Thu Jul 26 11:01:32 2012 +0200
@@ -1,7 +1,5 @@
 package org.hedgewars.hedgeroid.netplay;
 
-import java.util.Comparator;
-
 import org.hedgewars.hedgeroid.R;
 
 import android.content.res.Resources;
@@ -10,15 +8,13 @@
 	public static final String MAP_REGULAR = "+rnd+";
 	public static final String MAP_MAZE = "+maze+";
 	public static final String MAP_DRAWN = "+drawn+";
-	public static final Comparator<Room> ID_COMPARATOR = new ByIdComparator();
 	
 	public final String name, map, scheme, weapons, owner;
 	public final int playerCount, teamCount;
 	public final boolean inProgress;
-	public final long id;	// for ListView
 	
 	public Room(String name, String map, String scheme, String weapons,
-			String owner, int playerCount, int teamCount, boolean inProgress, long id) {
+			String owner, int playerCount, int teamCount, boolean inProgress) {
 		this.name = name;
 		this.map = map;
 		this.scheme = scheme;
@@ -27,7 +23,6 @@
 		this.playerCount = playerCount;
 		this.teamCount = teamCount;
 		this.inProgress = inProgress;
-		this.id = id;
 	}
 
 	public static String formatMapName(Resources res, String map) {
@@ -42,10 +37,4 @@
 		}
 		return map;
 	}
-	
-	private static final class ByIdComparator implements Comparator<Room> {
-		public int compare(Room lhs, Room rhs) {
-			return lhs.id<rhs.id ? -1 : lhs.id>rhs.id ? 1 : 0;
-		}
-	}
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomList.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomList.java	Thu Jul 26 11:01:32 2012 +0200
@@ -4,46 +4,43 @@
 import java.util.Map;
 import java.util.TreeMap;
 
-import org.hedgewars.hedgeroid.netplay.JnaFrontlib.RoomPtr;
-
 import android.database.DataSetObservable;
 import android.util.Log;
+import android.util.Pair;
 
 public class RoomList extends DataSetObservable {
 	private long nextId = 1;
-	private Map<String, Room> rooms = new TreeMap<String, Room>();
+	private Map<String, Pair<Room, Long>> rooms = new TreeMap<String, Pair<Room, Long>>();
 	
-	public void updateList(RoomPtr[] roomPtrs) {
-		Map<String, Room> newMap = new TreeMap<String, Room>();
-		for(RoomPtr roomPtr : roomPtrs) {
-			JnaFrontlib.Room room = roomPtr.deref();
-			Room oldEntry = rooms.get(room.name);
+	public void updateList(Room[] newRooms) {
+		Map<String, Pair<Room, Long>> newMap = new TreeMap<String, Pair<Room, Long>>();
+		for(Room room : newRooms) {
+			Pair<Room, Long> oldEntry = rooms.get(room.name);
 			if(oldEntry == null) {
-				newMap.put(room.name, buildRoom(room, nextId++));
+				newMap.put(room.name, Pair.create(room, nextId++));
 			} else {
-				newMap.put(room.name, buildRoom(room, oldEntry.id));
+				newMap.put(room.name, Pair.create(room, oldEntry.second));
 			}
 		}
 		rooms = newMap;
 		notifyChanged();
 	}
 	
-	public void addRoomWithNewId(RoomPtr roomPtr) {
-		putRoom(roomPtr.deref(), nextId++);
+	public void addRoomWithNewId(Room room) {
+		rooms.put(room.name, Pair.create(room, nextId++));
 		notifyChanged();
 	}
 	
-	public void updateRoom(String name, RoomPtr roomPtr) {
-		JnaFrontlib.Room room = roomPtr.deref();
-		Room oldEntry = rooms.get(name);
+	public void updateRoom(String name, Room room) {
+		Pair<Room, Long> oldEntry = rooms.get(name);
 		if(oldEntry == null) {
 			Log.e("RoomList", "Received update for unknown room: "+name);
-			putRoom(room, nextId++);
+			rooms.put(room.name, Pair.create(room, nextId++));
 		} else {
 			if(!name.equals(room.name)) {
 				rooms.remove(name);
 			}
-			putRoom(room, oldEntry.id);
+			rooms.put(room.name, Pair.create(room, oldEntry.second));
 		}
 		notifyChanged();
 	}
@@ -61,15 +58,7 @@
 		}
 	}
 	
-	public Map<String, Room> getMap() {
+	public Map<String, Pair<Room, Long>> getMap() {
 		return Collections.unmodifiableMap(rooms);
 	}
-	
-	private void putRoom(JnaFrontlib.Room r, long id) {
-		rooms.put(r.name, buildRoom(r, id));
-	}
-	
-	private Room buildRoom(JnaFrontlib.Room r, long id) {
-		return new Room(r.name, r.map, r.scheme, r.weapons, r.owner, r.playerCount, r.teamCount, r.inProgress, id);
-	}
 }
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomListAdapter.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomListAdapter.java	Thu Jul 26 11:01:32 2012 +0200
@@ -2,6 +2,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 import org.hedgewars.hedgeroid.R;
@@ -9,6 +10,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.database.DataSetObserver;
+import android.util.Pair;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -16,7 +18,7 @@
 import android.widget.TextView;
 
 public class RoomListAdapter extends BaseAdapter {
-	private List<Room> rooms = new ArrayList<Room>();
+	private List<Pair<Room, Long>> rooms = new ArrayList<Pair<Room, Long>>();
 	private Context context;
 	private RoomList roomList;
 	
@@ -41,11 +43,11 @@
 	}
 
 	public Room getItem(int position) {
-		return rooms.get(position);
+		return rooms.get(position).first;
 	}
 
 	public long getItemId(int position) {
-		return rooms.get(position).id;
+		return rooms.get(position).second;
 	}
 
 	public boolean hasStableIds() {
@@ -62,7 +64,6 @@
 	}
 	
 	public void invalidate() {
-		rooms = new ArrayList<Room>();
 		if(roomList != null) {
 			roomList.unregisterObserver(observer);
 		}
@@ -71,8 +72,8 @@
 	}
 	
 	private void reloadFromList(RoomList list) {
-		rooms = new ArrayList<Room>(roomList.getMap().values());
-		Collections.sort(rooms, Collections.reverseOrder(Room.ID_COMPARATOR));
+		rooms = new ArrayList<Pair<Room, Long>>(roomList.getMap().values());
+		Collections.sort(rooms, RoomAgeComparator.INSTANCE);
 		notifyDataSetChanged();
 	}
 	
@@ -91,7 +92,7 @@
 			v = vi.inflate(R.layout.listview_room, null);
 		}
 		
-		Room room = rooms.get(position);
+		Room room = rooms.get(position).first;
 		int iconRes = room.inProgress ? R.drawable.roomlist_ingame : R.drawable.roomlist_preparing;
 		
 		if(v.findViewById(android.R.id.text1) == null) {
@@ -128,4 +129,11 @@
 		
 		return v;
 	}
+	
+	private static final class RoomAgeComparator implements Comparator<Pair<Room, Long>> {
+		public static final RoomAgeComparator INSTANCE = new RoomAgeComparator();
+		public int compare(Pair<Room, Long> lhs, Pair<Room, Long> rhs) {
+			return rhs.second.compareTo(lhs.second);
+		}
+	}
 }
\ No newline at end of file
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomlistFragment.java	Tue Jul 24 16:57:48 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomlistFragment.java	Thu Jul 26 11:01:32 2012 +0200
@@ -1,15 +1,9 @@
 package org.hedgewars.hedgeroid.netplay;
 
 import org.hedgewars.hedgeroid.R;
-import org.hedgewars.hedgeroid.netplay.NetplayService.NetplayBinder;
 
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
 import android.os.Bundle;
 import android.os.CountDownTimer;
-import android.os.IBinder;
 import android.support.v4.app.ListFragment;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -17,21 +11,19 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
-import android.widget.AdapterView;
 import android.widget.Toast;
 
 public class RoomlistFragment extends ListFragment implements OnItemClickListener {
 	private static final int AUTO_REFRESH_INTERVAL_MS = 15000;
 	
-	private NetplayService service;
+	private Netplay netconn;
 	private RoomListAdapter adapter;
 	private CountDownTimer autoRefreshTimer = new CountDownTimer(Long.MAX_VALUE, AUTO_REFRESH_INTERVAL_MS) {
 		@Override
 		public void onTick(long millisUntilFinished) {
-			if(service != null && service.isConnected()) {
-				service.sendRoomlistRequest();
-			}
+			netconn.sendRoomlistRequest();
 		}
 		
 		@Override
@@ -41,9 +33,9 @@
 	@Override
 	public void onCreate(Bundle savedInstanceState) {
 		super.onCreate(savedInstanceState);
-		getActivity().bindService(new Intent(getActivity(), NetplayService.class), serviceConnection,
-	            Context.BIND_AUTO_CREATE);
+		netconn = Netplay.getAppInstance(getActivity().getApplicationContext());
 		adapter = new RoomListAdapter(getActivity());
+		adapter.setList(netconn.roomList);
 		setListAdapter(adapter);
 		setHasOptionsMenu(true);
 	}
@@ -51,8 +43,7 @@
 	@Override
 	public View onCreateView(LayoutInflater inflater, ViewGroup container,
 			Bundle savedInstanceState) {
-		View v = inflater.inflate(R.layout.lobby_rooms_fragment, container, false);
-		return v;
+		return inflater.inflate(R.layout.lobby_rooms_fragment, container, false);
 	}
 	
 	@Override
@@ -64,10 +55,7 @@
 	@Override
 	public void onResume() {
 		super.onResume();
-		if(service != null) {
-			service.sendRoomlistRequest();
-			autoRefreshTimer.start();
-		}
+		autoRefreshTimer.start();
 	}
 	
 	@Override
@@ -79,7 +67,7 @@
 	@Override
 	public void onDestroy() {
 		super.onDestroy();
-		getActivity().unbindService(serviceConnection);
+		adapter.invalidate();
 	}
 	
 	@Override
@@ -92,9 +80,7 @@
 	public boolean onOptionsItemSelected(MenuItem item) {
 		switch(item.getItemId()) {
 		case R.id.roomlist_refresh:
-			if(service != null && service.isConnected()) {
-				service.sendRoomlistRequest();
-			}
+			netconn.sendRoomlistRequest();
 			return true;
 		default:
 			return super.onOptionsItemSelected(item);
@@ -104,18 +90,4 @@
 	public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
 		Toast.makeText(getActivity(), R.string.not_implemented_yet, Toast.LENGTH_SHORT).show();
 	}
-	
-    private ServiceConnection serviceConnection = new ServiceConnection() {
-        public void onServiceConnected(ComponentName className, IBinder binder) {
-        	service = ((NetplayBinder) binder).getService();
-        	adapter.setList(service.roomList);
-        	autoRefreshTimer.start();
-        }
-
-        public void onServiceDisconnected(ComponentName className) {
-        	// TODO navigate away
-        	adapter.invalidate();
-        	service = null;
-        }
-    };
 }