Hedgeroid: Misguided attempts at getting the connection lifecycle right.
Committing this because it basically runs this way, so i might want to revert to
it later :)
--- a/project_files/Android-build/SDL-android-project/res/layout/activity_lobby.xml Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/res/layout/activity_lobby.xml Tue Jul 24 16:57:48 2012 +0200
@@ -1,36 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent">
- <include layout="@layout/background"/>
- <TabHost
- android:id="@android:id/tabhost"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" >
+
+ <include layout="@layout/background" />
- <LinearLayout
- android:orientation="vertical"
+ <TabHost
+ android:id="@android:id/tabhost"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal" >
+
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="0" />
- <TabWidget
- android:id="@android:id/tabs"
- android:orientation="horizontal"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1" >
+
+ <fragment
+ android:id="@+id/roomListFragment"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ class="org.hedgewars.hedgeroid.netplay.RoomlistFragment"
+ tools:layout="@layout/lobby_rooms_fragment" />
- <FrameLayout
- android:id="@android:id/tabcontent"
- android:layout_width="0dp"
- android:layout_height="0dp"/>
+ <fragment
+ android:id="@+id/chatFragment"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ class="org.hedgewars.hedgeroid.netplay.LobbyChatFragment"
+ tools:layout="@layout/lobby_chat_fragment" />
- <android.support.v4.view.ViewPager
- android:id="@+id/pager"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1"/>
+ <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" />
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
- </LinearLayout>
-</TabHost>
</FrameLayout>
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/project_files/Android-build/SDL-android-project/res/layout/tab_indicator.xml Tue Jul 24 16:57:48 2012 +0200
@@ -0,0 +1,22 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="64dip"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:layout_marginTop="-3dip"
+ android:layout_marginBottom="-3dip"
+ android:orientation="vertical">
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ />
+
+ <TextView android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ style="?android:attr/tabWidgetStyle"
+ />
+</RelativeLayout>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/project_files/Android-build/SDL-android-project/res/layout/vertical_tabs.xml Tue Jul 24 16:57:48 2012 +0200
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/tabhost"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal" >
+
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_weight="0" />
+
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="0dip"
+ android:layout_height="match_parent"
+ android:layout_weight="1" />
+ </LinearLayout>
+</TabHost>
\ No newline at end of file
--- a/project_files/Android-build/SDL-android-project/res/menu/lobby_options.xml Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/res/menu/lobby_options.xml Tue Jul 24 16:57:48 2012 +0200
@@ -3,4 +3,8 @@
android:id="@+id/room_create"
android:title="@string/lobby_roomlistmenu_create"
android:showAsAction="ifRoom" />
+ <item
+ android:id="@+id/disconnect"
+ android:title="@string/lobby_menu_disconnect"
+ android:showAsAction="ifRoom" />
</menu>
\ No newline at end of file
--- a/project_files/Android-build/SDL-android-project/res/values/strings.xml Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/res/values/strings.xml Tue Jul 24 16:57:48 2012 +0200
@@ -96,6 +96,15 @@
<string name="lobby_playerlist_contextmenu_follow">Follow</string>
<string name="lobby_roomlistmenu_create">Create room</string>
<string name="lobby_roomlistmenu_refresh">Refresh</string>
+ <string name="lobby_menu_disconnect">Disconnect</string>
<string name="not_implemented_yet">Sorry, not implemented yet. :(</string>
+
+ <!-- Errors -->
+ <string name="error_connection_failed">Unable to connect to the server.</string>
+ <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>
+
</resources>
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/MainActivity.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/MainActivity.java Tue Jul 24 16:57:48 2012 +0200
@@ -25,16 +25,21 @@
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 android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
+import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
@@ -63,7 +68,11 @@
startGame.setOnClickListener(startGameClicker);
joinLobby.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
- showDialog(DIALOG_START_NETGAME);
+ if(!NetplayService.isActive()) {
+ showDialog(DIALOG_START_NETGAME);
+ } else {
+ startActivity(new Intent(getApplicationContext(), LobbyActivity.class));
+ }
}
});
@@ -141,10 +150,11 @@
edit.putString(PREF_PLAYERNAME, playerName);
edit.commit();
- // TODO actually use that name
- Intent netplayIntent = new Intent(getApplicationContext(), LobbyActivity.class);
- netplayIntent.putExtra("playerName", playerName);
- startActivity(netplayIntent);
+ 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));
}
}
});
@@ -170,4 +180,11 @@
startActivity(new Intent(getApplicationContext(), StartGameActivity.class));
}
};
+
+ private BroadcastReceiver connectedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ startActivity(new Intent(getApplicationContext(), LobbyActivity.class));
+ }
+ };
}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyActivity.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyActivity.java Tue Jul 24 16:57:48 2012 +0200
@@ -1,51 +1,113 @@
package org.hedgewars.hedgeroid.netplay;
-import java.util.ArrayList;
-
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.support.v4.app.Fragment;
+import android.os.IBinder;
import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentPagerAdapter;
-import android.support.v4.view.ViewPager;
+import android.support.v4.content.LocalBroadcastManager;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
-import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.TabHost;
-import android.widget.TabWidget;
+import android.widget.TextView;
+import android.widget.Toast;
public class LobbyActivity extends FragmentActivity {
- TabHost mTabHost;
- ViewPager mViewPager;
- TabsAdapter mTabsAdapter;
-
+ private TabHost tabHost;
+ private NetplayService service;
+
+ 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();
+ finish();
+ }
+ };
+
@Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
+ 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);
setContentView(R.layout.activity_lobby);
- mTabHost = (TabHost)findViewById(android.R.id.tabhost);
- if(mTabHost != null) {
- mTabHost.setup();
-
- mViewPager = (ViewPager)findViewById(R.id.pager);
-
- mTabsAdapter = new TabsAdapter(this, mTabHost, mViewPager);
+ tabHost = (TabHost)findViewById(android.R.id.tabhost);
+ if(tabHost != null) {
+ tabHost.setup();
+ tabHost.getTabWidget().setOrientation(LinearLayout.VERTICAL);
+
+ tabHost.addTab(tabHost.newTabSpec("rooms").setIndicator(createIndicatorView(tabHost, "Rooms", null)).setContent(R.id.roomListFragment));
+ tabHost.addTab(tabHost.newTabSpec("chat").setIndicator(createIndicatorView(tabHost, "Chat", null)).setContent(R.id.chatFragment));
+ tabHost.addTab(tabHost.newTabSpec("players").setIndicator(createIndicatorView(tabHost, "Players", null)).setContent(R.id.playerListFragment));
- mTabsAdapter.addTab(mTabHost.newTabSpec("roomlist").setIndicator("Rooms"),
- RoomlistFragment.class, null);
- mTabsAdapter.addTab(mTabHost.newTabSpec("chat").setIndicator("Chat"),
- LobbyChatFragment.class, null);
- mTabsAdapter.addTab(mTabHost.newTabSpec("players").setIndicator("Players"),
- PlayerlistFragment.class, null);
-
- if (savedInstanceState != null) {
- mTabHost.setCurrentTabByTag(savedInstanceState.getString("tab"));
+ if (icicle != null) {
+ tabHost.setCurrentTabByTag(icicle.getString("currentTab"));
}
}
}
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(disconnectReceiver);
+ unbindService(serviceConnection);
+ }
+
+ private View createIndicatorView(TabHost tabHost, CharSequence label, Drawable icon) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ View tabIndicator = inflater.inflate(R.layout.tab_indicator,
+ tabHost.getTabWidget(), // tab widget is the parent
+ false); // no inflate params
+
+ final TextView tv = (TextView) tabIndicator.findViewById(R.id.title);
+ tv.setText(label);
+
+ if(icon != null) {
+ final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.icon);
+ iconView.setImageDrawable(icon);
+ }
+
+ return tabIndicator;
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ getMenuInflater().inflate(R.menu.lobby_options, menu);
+ return true;
+ }
+
+ @Override
+ 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();
+ return true;
+ case R.id.disconnect:
+ if(service != null && service.isConnected()) {
+ service.disconnect();
+ }
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
/*@Override
protected void onCreate(Bundle arg0) {
super.onCreate(arg0);
@@ -78,108 +140,20 @@
}*/
@Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- if(mTabHost != null) {
- outState.putString("tab", mTabHost.getCurrentTabTag());
+ protected void onSaveInstanceState(Bundle icicle) {
+ super.onSaveInstanceState(icicle);
+ if(tabHost != null) {
+ icicle.putString("currentTab", tabHost.getCurrentTabTag());
}
}
-
- /**
- * This is a helper class that implements the management of tabs and all
- * details of connecting a ViewPager with associated TabHost. It relies on a
- * trick. Normally a tab host has a simple API for supplying a View or
- * Intent that each tab will show. This is not sufficient for switching
- * between pages. So instead we make the content part of the tab host
- * 0dp high (it is not shown) and the TabsAdapter supplies its own dummy
- * view to show as the tab content. It listens to changes in tabs, and takes
- * care of switch to the correct paged in the ViewPager whenever the selected
- * tab changes.
- */
- public static class TabsAdapter extends FragmentPagerAdapter
- implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
- private final Context mContext;
- private final TabHost mTabHost;
- private final ViewPager mViewPager;
- private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
-
- static final class TabInfo {
- private final Class<?> clss;
- private final Bundle args;
-
- TabInfo(Class<?> _class, Bundle _args) {
- clss = _class;
- args = _args;
- }
- }
-
- static class DummyTabFactory implements TabHost.TabContentFactory {
- private final Context mContext;
-
- public DummyTabFactory(Context context) {
- mContext = context;
- }
-
- public View createTabContent(String tag) {
- View v = new View(mContext);
- v.setMinimumWidth(0);
- v.setMinimumHeight(0);
- return v;
- }
+
+ private ServiceConnection serviceConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder binder) {
+ service = ((NetplayBinder) binder).getService();
}
- public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager pager) {
- super(activity.getSupportFragmentManager());
- mContext = activity;
- mTabHost = tabHost;
- mViewPager = pager;
- mTabHost.setOnTabChangedListener(this);
- mViewPager.setAdapter(this);
- mViewPager.setOnPageChangeListener(this);
- }
-
- public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle args) {
- tabSpec.setContent(new DummyTabFactory(mContext));
-
- TabInfo info = new TabInfo(clss, args);
- mTabs.add(info);
- mTabHost.addTab(tabSpec);
- notifyDataSetChanged();
- }
-
- @Override
- public int getCount() {
- return mTabs.size();
+ public void onServiceDisconnected(ComponentName className) {
+ service = null;
}
-
- @Override
- public Fragment getItem(int position) {
- TabInfo info = mTabs.get(position);
- return Fragment.instantiate(mContext, info.clss.getName(), info.args);
- }
-
- public void onTabChanged(String tabId) {
- int position = mTabHost.getCurrentTab();
- mViewPager.setCurrentItem(position);
- }
-
- public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
- }
-
- public void onPageSelected(int position) {
- // Unfortunately when TabHost changes the current tab, it kindly
- // also takes care of putting focus on it when not in touch mode.
- // The jerk.
- // This hack tries to prevent this from pulling focus out of our
- // ViewPager.
- TabWidget widget = mTabHost.getTabWidget();
- int oldFocusability = widget.getDescendantFocusability();
- widget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
- mTabHost.setCurrentTab(position);
- widget.setDescendantFocusability(oldFocusability);
- }
-
- public void onPageScrollStateChanged(int state) {
- }
- }
+ };
}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyChatFragment.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/LobbyChatFragment.java Tue Jul 24 16:57:48 2012 +0200
@@ -26,13 +26,13 @@
private EditText editText;
private ListView listView;
private ChatlogAdapter adapter;
- private Netconn netconn;
+ private NetplayService service;
private void commitText() {
String text = editText.getText().toString();
- if(netconn != null && netconn.isConnected() && text.length()>0) {
+ if(service != null && service.isConnected() && text.length()>0) {
editText.setText("");
- netconn.sendChat(text);
+ service.sendChat(text);
}
}
@@ -84,15 +84,15 @@
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
Log.d("LobbyChatFragment", "netconn received");
- netconn = ((NetplayBinder) binder).getNetconn();
- adapter.setLog(netconn.lobbyChatlog.getLog());
- netconn.lobbyChatlog.registerObserver(adapter);
+ service = ((NetplayBinder) binder).getService();
+ adapter.setLog(service.lobbyChatlog.getLog());
+ service.lobbyChatlog.registerObserver(adapter);
}
public void onServiceDisconnected(ComponentName className) {
// TODO navigate away
- netconn.lobbyChatlog.unregisterObserver(adapter);
- netconn = null;
+ service.lobbyChatlog.unregisterObserver(adapter);
+ service = null;
}
};
}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/Netconn.java Mon Jul 23 00:17:06 2012 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,204 +0,0 @@
-package org.hedgewars.hedgeroid.netplay;
-
-import java.io.File;
-import java.io.IOException;
-
-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.content.Context;
-import android.util.Log;
-
-/**
- * Java-wrapper for the C netconn type. Apart from turning netconn into a more Java-like
- * object with methods, this also handles some of the problems of C <-> Java interop (e.g.
- * ensuring that callback objects don't get garbage collected).
- */
-public class Netconn {
- private static final JnaFrontlib FLIB = Flib.INSTANCE;
- private static final String DEFAULT_SERVER = "140.247.62.101";
- private static final int DEFAULT_PORT = 46631;
-
- private NetconnPtr conn;
- private String playerName;
- 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 final MessageLog lobbyChatlog;
- public final MessageLog roomChatlog;
-
- 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) {
- // TODO I guess more needs to happen here...
- 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 arg1, String arg2) {
- FLIB.flib_netconn_destroy(conn);
- conn = null;
- }
- };
-
- /**
- * Connect to the official Hedgewars server.
- *
- * @throws IOException if the metascheme file can't be read or the connection to the server fails
- */
- public Netconn(Context context, String playerName) throws IOException {
- this(context, playerName, DEFAULT_SERVER, DEFAULT_PORT);
- }
-
- /**
- * Connect to the server with the given hostname and port
- *
- * @throws IOException if the metascheme file can't be read or the connection to the server fails
- */
- public Netconn(Context context, String playerName, String host, int port) throws IOException {
- if(playerName == null) {
- playerName = "Player";
- }
- this.playerName = playerName;
- this.lobbyChatlog = new MessageLog(context);
- this.roomChatlog = new MessageLog(context);
-
- MetaschemePtr meta = null;
- File dataPath = Utils.getDataPathFile(context);
- try {
- String metaschemePath = new File(dataPath, "metasettings.ini").getAbsolutePath();
- meta = FLIB.flib_metascheme_from_ini(metaschemePath);
- if(meta == null) {
- throw new IOException("Missing metascheme");
- }
- conn = FLIB.flib_netconn_create(playerName, meta, dataPath.getAbsolutePath(), host, port);
- if(conn == null) {
- throw new IOException("Unable to connect to the server");
- }
- 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);
- } finally {
- FLIB.flib_metascheme_release(meta);
- }
- }
-
- public void disconnect() {
- if(conn != null) {
- FLIB.flib_netconn_send_quit(conn, "User quit");
- FLIB.flib_netconn_destroy(conn);
- conn = null;
- }
- }
-
- public void tick() {
- FLIB.flib_netconn_tick(conn);
- }
-
- 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;
- }
-
- @Override
- protected void finalize() throws Throwable {
- if(conn != null) {
- FLIB.flib_netconn_destroy(conn);
- Log.e("Netconn", "Leaked Netconn object");
- }
- }
-
-}
\ No newline at end of file
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/NetplayService.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/NetplayService.java Tue Jul 24 16:57:48 2012 +0200
@@ -1,66 +1,317 @@
package org.hedgewars.hedgeroid.netplay;
-import java.io.IOException;
+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.CountDownTimer;
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();
- public Netconn netconn;
- private CountDownTimer timer;
+ 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", "Creating");
- if(Flib.INSTANCE.flib_init() != 0) {
- throw new RuntimeException("Unable to start frontlib");
- }
- try {
- netconn = new Netconn(getApplicationContext(), "AndroidTester");
- } catch (IOException e) {
- // TODO better handling
- throw new RuntimeException("Unable to start frontlib", e);
- }
- timer = new CountDownTimer(Long.MAX_VALUE, 50) {
- @Override
- public void onTick(long millisUntilFinished) {
- if(netconn != null && netconn.isConnected()) {
- netconn.tick();
+ 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);
}
}
-
- @Override
- public void onFinish() {
- }
- };
- timer.start();
+ });
+ if(Flib.INSTANCE.flib_init() != 0) {
+ stopWithError(getString(R.string.error_unexpected, "Unable to start frontlib"));
+ }
}
@Override
public void onDestroy() {
- Log.d("NetplayService", "Destroying");
- timer.cancel();
- netconn.disconnect();
+ 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 {
- Netconn getNetconn() {
- return netconn;
+ 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 boolean isConnected() {
- return netconn!=null;
+ public static boolean isActive() {
+ return instance!=null;
}
}
+
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerList.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerList.java Tue Jul 24 16:57:48 2012 +0200
@@ -22,6 +22,13 @@
}
}
+ public void clear() {
+ if(!players.isEmpty()) {
+ players.clear();
+ notifyChanged();
+ }
+ }
+
public Map<String, Player> getMap() {
return Collections.unmodifiableMap(players);
}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerlistFragment.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/PlayerlistFragment.java Tue Jul 24 16:57:48 2012 +0200
@@ -4,7 +4,6 @@
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;
@@ -21,14 +20,13 @@
import android.widget.AdapterView.AdapterContextMenuInfo;
public class PlayerlistFragment extends ListFragment {
- private Netconn netconn;
+ private NetplayService netplayService;
private PlayerListAdapter playerListAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- getActivity().bindService(new Intent(getActivity(), NetplayService.class), serviceConnection,
- Context.BIND_AUTO_CREATE);
+ getActivity().bindService(new Intent(getActivity(), NetplayService.class), serviceConnection, 0);
playerListAdapter = new PlayerListAdapter(getActivity());
setListAdapter(playerListAdapter);
}
@@ -53,8 +51,8 @@
switch(item.getItemId()) {
case R.id.player_info:
Player p = playerListAdapter.getItem(info.position);
- if(netconn != null) {
- netconn.sendPlayerInfoQuery(p.name);
+ if(netplayService != null) {
+ netplayService.sendPlayerInfoQuery(p.name);
}
return true;
case R.id.player_follow:
@@ -79,14 +77,14 @@
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
- netconn = ((NetplayBinder) binder).getNetconn();
- playerListAdapter.setList(netconn.playerList);
+ netplayService = ((NetplayBinder) binder).getService();
+ playerListAdapter.setList(netplayService.playerList);
}
public void onServiceDisconnected(ComponentName className) {
// TODO navigate away
playerListAdapter.invalidate();
- netconn = null;
+ netplayService = null;
}
};
}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomList.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomList.java Tue Jul 24 16:57:48 2012 +0200
@@ -54,6 +54,13 @@
}
}
+ public void clear() {
+ if(!rooms.isEmpty()) {
+ rooms.clear();
+ notifyChanged();
+ }
+ }
+
public Map<String, Room> getMap() {
return Collections.unmodifiableMap(rooms);
}
--- a/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomlistFragment.java Mon Jul 23 00:17:06 2012 +0200
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/RoomlistFragment.java Tue Jul 24 16:57:48 2012 +0200
@@ -24,13 +24,13 @@
public class RoomlistFragment extends ListFragment implements OnItemClickListener {
private static final int AUTO_REFRESH_INTERVAL_MS = 15000;
- private Netconn netconn;
+ private NetplayService service;
private RoomListAdapter adapter;
private CountDownTimer autoRefreshTimer = new CountDownTimer(Long.MAX_VALUE, AUTO_REFRESH_INTERVAL_MS) {
@Override
public void onTick(long millisUntilFinished) {
- if(netconn != null && netconn.isConnected()) {
- netconn.sendRoomlistRequest();
+ if(service != null && service.isConnected()) {
+ service.sendRoomlistRequest();
}
}
@@ -64,8 +64,8 @@
@Override
public void onResume() {
super.onResume();
- if(netconn != null) {
- netconn.sendRoomlistRequest();
+ if(service != null) {
+ service.sendRoomlistRequest();
autoRefreshTimer.start();
}
}
@@ -92,8 +92,8 @@
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.roomlist_refresh:
- if(netconn != null) {
- netconn.sendRoomlistRequest();
+ if(service != null && service.isConnected()) {
+ service.sendRoomlistRequest();
}
return true;
default:
@@ -107,15 +107,15 @@
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
- netconn = ((NetplayBinder) binder).getNetconn();
- adapter.setList(netconn.roomList);
+ service = ((NetplayBinder) binder).getService();
+ adapter.setList(service.roomList);
autoRefreshTimer.start();
}
public void onServiceDisconnected(ComponentName className) {
// TODO navigate away
adapter.invalidate();
- netconn = null;
+ service = null;
}
};
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/project_files/Android-build/SDL-android-project/src/org/hedgewars/hedgeroid/netplay/TickHandler.java Tue Jul 24 16:57:48 2012 +0200
@@ -0,0 +1,54 @@
+package org.hedgewars.hedgeroid.netplay;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+/**
+ * This class handles regularly calling a specified runnable
+ * on the looper provided in the constructor. The first call
+ * occurs without delay (though still via the looper), all
+ * following calls are delayed by (approximately) the interval.
+ * The interval can be changed at any time, which will cause
+ * an immediate execution of the runnable again.
+ */
+class TickHandler extends Handler {
+ private final Runnable callback;
+ private int messageId;
+ private long interval;
+ private boolean running;
+
+ public TickHandler(Looper looper, long interval, Runnable callback) {
+ super(looper);
+ this.callback = callback;
+ this.interval = interval;
+ }
+
+ public synchronized void stop() {
+ messageId++;
+ running = false;
+ }
+
+ public synchronized void start() {
+ messageId++;
+ sendMessage(obtainMessage(messageId));
+ running = true;
+ }
+
+ public synchronized void setInterval(long interval) {
+ this.interval = interval;
+ if(running) {
+ start();
+ }
+ }
+
+ @Override
+ public synchronized void handleMessage(Message msg) {
+ if(msg.what == messageId) {
+ callback.run();
+ }
+ if(msg.what == messageId) {
+ sendMessageDelayed(obtainMessage(messageId), interval);
+ }
+ }
+}
\ No newline at end of file