public class CtFwSGameState {
+ public interface TimerProvider {
+ long wallMS();
+ void postDelay(Runnable r, long delayMS);
+ void cancelPost(Runnable r);
+ }
+ private TimerProvider mT;
+
+ public CtFwSGameState (TimerProvider t) {
+ mT = t;
+ }
+
// Game time
private boolean configured = false;
}
break;
}
- if (!isMessageTimeWithin(lastMsgTimestamp)) {
- msgs.clear();
- notifyMessages();
- }
- notifyConfig();
+ notifyConfigEtAl();
}
public String toMqttConfigMessage() {
if (!configured) {
}
public void deconfigure() {
this.configured = false;
- notifyConfig();
+ notifyConfigEtAl();
}
public void setEndT(long endT) {
this.endT = endT;
- notifyConfig();
+ notifyConfigEtAl();
}
public class Now {
public boolean stop = false;
public int round = 0; // 0 for setup
public long roundStart = 0, roundEnd = 0; // NTP seconds
+
+ public long wallMS; // timestamp at object creation: NTP time * 1000 (i.e. msec)
}
- public Now getNow(long now) {
+ public Now getNow(long wallMS) {
Now res = new Now();
+ res.wallMS = wallMS;
+
+ long now = wallMS/1000;
if (!configured) {
- res.rationale = "Game not configured";
+ res.rationale = "Game not configured!";
res.stop = true;
} else if (endT >= startT) {
res.rationale = "Game over!";
res.stop = true;
- } else if (now <= startT) {
+ } else if (now < startT) {
res.rationale = "Start time in the future!";
- res.roundStart = startT;
+ res.roundStart = res.roundEnd = startT;
}
if (res.rationale != null) {
return res;
} catch (NoSuchElementException nse) {
// Maybe they forgot a time stamp. That's not ideal, but... fake it?
// XXX Back off a bit, for time sync reasons
- lastMsgTimestamp = System.currentTimeMillis()/1000 - 30;
+ lastMsgTimestamp = mT.wallMS()/1000 - 30;
msgs.add(new Msg(lastMsgTimestamp, str));
notifyMessages();
return;
public interface Observer {
void onCtFwSConfigure(CtFwSGameState game);
+ void onCtFwSNow(CtFwSGameState game, Now now);
void onCtFwSFlags(CtFwSGameState game);
void onCtFwSMessage(CtFwSGameState game, List<Msg> msgs);
-
}
final private Set<Observer> mObsvs = new HashSet<>();
private void notifyFlags() {
for (Observer o : mObsvs) { o.onCtFwSMessage(this, msgs); }
}
}
- private void notifyConfig() {
+ private void notifyConfigEtAl() {
+ if (!isMessageTimeWithin(lastMsgTimestamp)) {
+ msgs.clear();
+ notifyMessages();
+ }
synchronized(this) {
for (Observer o : mObsvs) { o.onCtFwSConfigure(this); }
}
+ notifyNow();
+ }
+ private final Runnable futureNotifyNow = new Runnable() {
+ @Override
+ public void run() {
+ notifyNow();
+ }
+ };
+ private void notifyNow() {
+ mT.cancelPost(futureNotifyNow);
+ Now n = getNow(mT.wallMS());
+ synchronized(this) {
+ for (Observer o : mObsvs) {
+ o.onCtFwSNow(this, n);
+ }
+ if (n.rationale == null || !n.stop) {
+ mT.postDelay(futureNotifyNow, n.roundEnd*1000 - n.wallMS);
+ }
+ }
}
public void registerObserver(Observer d) {
synchronized(this) { mObsvs.add(d); }
public void unregisterObserver(Observer d) {
synchronized(this) { mObsvs.remove(d); }
}
-
}
applicationId "com.acmetensortoys.ctfwstimer"
minSdkVersion 19
targetSdkVersion 25
- versionCode 1
+ versionCode 3
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
dimension "play"
}
}
+
+ // TODO: Should filter out play+debug since it will overflow dex method
+ // limits (yikes)
}
repositories {
compile 'com.android.support:appcompat-v7:25.1.1'
compile 'com.android.support:support-v4:25.1.1'
+ // Needed for wear data synchronization; blech
playCompile 'com.google.android.gms:play-services:10.0.1'
compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.1-SNAPSHOT'
<!-- Required for Paho MQTT client -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
- <!-- Seems like a reasonable idea? -->
- <!-- <uses-permission android:name="android.permission.VIBRATE" /> -->
+ <!-- Seems like a reasonable idea -->
+ <uses-permission android:name="android.permission.VIBRATE" />
+
<application
android:allowBackup="true"
android:icon="@mipmap/ic_shield_1"
<service android:name="org.eclipse.paho.android.service.MqttService" />
- <activity android:name=".AboutActivity"></activity>
+ <activity android:name=".AboutActivity" />
+
+ <service
+ android:name=".MainService"
+ android:enabled="true"
+ android:exported="false"></service>
</application>
</manifest>
\ No newline at end of file
}
private Runnable mProber;
-
@Override
public void onCtFwSConfigure(final CtFwSGameState gs) {
- final long nowMS = System.currentTimeMillis();
- long nowET = SystemClock.elapsedRealtime(); // Chronometer timebase
- final long tbcf = nowMS - nowET; // time base correction factor ("when we booted"-ish)
-
- final CtFwSGameState.Now now = gs.getNow(nowMS / 1000);
- final Runnable prober = new Runnable() {
- @Override
- public void run() {
- onCtFwSConfigure(gs);
- }
- };
+ }
+ @Override
+ public void onCtFwSNow(final CtFwSGameState gs, final CtFwSGameState.Now now) {
+ // time base correction factor ("when we booted"-ish)
+ final long tbcf = System.currentTimeMillis() - SystemClock.elapsedRealtime();
- Log.d("CtFwS", "Display game state; nowMS=" + nowMS + " r=" + now.round + " rs=" + now.roundStart + " re=" + now.roundEnd);
+ Log.d("CtFwS", "Display game state; nowMS=" + now.wallMS + " r=" + now.round + " rs=" + now.roundStart + " re=" + now.roundEnd);
if (now.rationale != null) {
Log.d("CtFwS", "Rationale: " + now.rationale + " stop=" + now.stop);
-
// TODO: display rationale somewhere, probably by hiding the game state!
-
doReset();
-
- if (mProber != null) {
- mHandler.removeCallbacks(mProber);
- }
- if (!now.stop) {
- mProber = prober;
- mHandler.postDelayed(mProber, now.roundStart*1000 - nowMS);
- }
-
return;
}
-
// Otherwise, it's game on!
- // Schedule a callback around the time of the next round; if we're early,
- // that's fine, we'll schedule it again. If we're late, it'll be glitchy,
- // but that's fine.
- mHandler.removeCallbacks(mProber);
- mProber = prober;
- mHandler.postDelayed(mProber, now.roundEnd * 1000 - nowMS);
-
{
final TextView tv_jb = (TextView) (mAct.findViewById(R.id.tv_jailbreak));
tv_jb.post(new Runnable() {
} else {
tv_jb.setText(
String.format(mAct.getResources().getString(R.string.ctfws_jailbreak),
- now.round));
+ now.round, gs.getRounds() - 1));
}
}
});
package com.acmetensortoys.ctfwstimer;
import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.Handler;
-import android.support.annotation.Nullable;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
import android.support.annotation.StringRes;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
-import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
-import org.eclipse.paho.android.service.MqttAndroidClient;
-import org.eclipse.paho.client.mqttv3.IMqttActionListener;
-import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
-import org.eclipse.paho.client.mqttv3.IMqttToken;
-import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
-import org.eclipse.paho.client.mqttv3.MqttClient;
-import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
-import org.eclipse.paho.client.mqttv3.MqttException;
-import org.eclipse.paho.client.mqttv3.MqttMessage;
-
-import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
-
public class MainActivity extends AppCompatActivity {
- private MqttAndroidClient mMqc;
-
- private final CtFwSGameState mCgs = new CtFwSGameState();
- private CtFwSCallbacksMQTT mCtfwscbs ; // set in onCreate
+ // TODO surely this belongs somewhere else
+ private static final String defserver = "tcp://ctfws-mqtt.ietfng.org:1883";
private MainActivityBuildHooks mabh = new MainActivityBuildHooksImpl();
- private TextView mTvSU; // set in onCreate
- private TextView mTvSS; // set in onCreate
- private void setServerStateText(@StringRes final int resid) {
- mTvSS.post(new Runnable() {
- @Override
- public void run() { mTvSS.setText(resid); }
- });
- }
-
- /*
- // Trace MQTT state
- private final MqttTraceHandler mqttth = new MqttTraceHandler() {
- @Override
- public void traceDebug(String tag, String message) {
- Log.d("CtFwSMqtt:"+tag,message);
- }
-
- @Override
- public void traceError(String tag, String message) {
- Log.e("CtFwSMqtt:"+tag,message);
- }
-
- @Override
- public void traceException(String tag, String message, Exception e) {
- Log.e("CtFwSMqtt:"+tag,message,e);
- }
- };
- */
- // We'll use this common callback object for our subscriptions below
- private final IMqttActionListener subal = new IMqttActionListener() {
- @Override
- public void onSuccess(IMqttToken asyncActionToken) {
- Log.d("CtFwS", "Sub OK: " + asyncActionToken);
- }
-
- @Override
- public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
- Log.e("CtFws", "Sub Fail: " + asyncActionToken, exception);
- }
- };
- // And this handles making our subscriptions for us
- private final MqttCallbackExtended mqttcb = new MqttCallbackExtended() {
- @Override
- public void connectComplete(boolean reconnect, String serverURI) {
- Log.d("CtFwS", "Conn OK 2 srv=" + serverURI + " reconn=" + reconnect);
- try {
- String p = "ctfws/game/";
- mMqc.subscribe(p+"config" , 2, null, subal, mCtfwscbs.onConfig);
- mMqc.subscribe(p+"endtime" , 2, null, subal, mCtfwscbs.onEnd);
- mMqc.subscribe(p+"flags" , 2, null, subal, mCtfwscbs.onFlags);
- mMqc.subscribe(p+"message" , 2, null, subal, mCtfwscbs.onMessage);
- mMqc.subscribe(p+"message/player", 2, null, subal, mCtfwscbs.onPlayerMessage);
- setServerStateText(R.string.mqtt_subbed);
- } catch (MqttException e) {
- Log.e("CtFwS", "Exn Sub", e);
- }
- }
-
- @Override
- public void connectionLost(Throwable cause) {
- Log.d("CtFwS", "Conn Lost: " + cause, cause);
- setServerStateText(R.string.mqtt_disconn);
-
- }
-
- @Override
- public void messageArrived(String topic, MqttMessage message) throws Exception {
- Log.d("CtFwS", "Message(Generic) " + topic + " : '" + message + "'" );
- }
-
- @Override
- public void deliveryComplete(IMqttDeliveryToken token) {
- // Unused, as we never publish
- Log.d("CtFwS", "Delivery OK");
- }
- };
- // And this handles yet more about connecting
- private final IMqttActionListener mqttal = new IMqttActionListener() {
- @Override
- public void onSuccess(IMqttToken asyncActionToken) {
- Log.d("CtFwS", "Conn OK 1");
- setServerStateText(R.string.mqtt_conn);
- }
-
+ private MainService.LocalBinder mSrvBinder; // set once connection completed
+ private MainService.Observer mSrvObs = new MainService.Observer() {
@Override
- public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
- Log.e("CtFws", "Conn Fail", exception);
- setServerStateText(R.string.mqtt_disconn);
- }
- };
-
- private synchronized void doMqtt(@Nullable String server) {
- // Hang up on an existing connection, if we have one
- synchronized (this) {
- if (mMqc != null) {
- if (mMqc.isConnected()) {
- try {
- mMqc.disconnect();
- Log.d("CtFwS", "domqtt disconnected");
- } catch (MqttException me) {
- Log.e("CtFwS", "domqtt disconn exn", me);
+ public void onMqttServerChanged(MainService.LocalBinder b, final String sURL) {
+ mTvSU.post(new Runnable() {
+ @Override
+ public void run() {
+ if (sURL == null) {
+ mTvSU.setText(R.string.string_null);
+ } else {
+ mTvSU.setText(sURL);
}
}
- }
- mMqc = null;
- mCgs.deconfigure();
- }
-
- // If that's all we were told to do, we're done
- if (server == null) {
- mTvSU.setText(R.string.string_null);
- return;
+ });
}
- mTvSU.setText(server);
-
- // Make our MQTT client and grab callbacks on *everything in sight*
- //
- // XXX For reasons beyond my understanding, we have to use a new client ID every time
- // or we won't resubscribe. I think this is github issue eclipse/paho.mqtt.android#170
- // but heavens only knows. Whatever, this works for the moment and doesn't leave
- // stragglers on my server as far as I can tell.
- MqttAndroidClient mqc = new MqttAndroidClient(this,server,MqttClient.generateClientId());
- mqc.setCallback(mqttcb);
- /*
- // Debugging aid: trace the paho client internals
- mqc.setTraceCallback(mqttth);
- mqc.setTraceEnabled(true);
- */
-
- // Ahem. Now then. Connect with *more callbacks*, which will fire off our
- // subscription requests, which of course have *yet more* callbacks, which
- // react to messages sent to us. Have we lost the thread yet?
- try {
- MqttConnectOptions mco = new MqttConnectOptions();
- mco.setCleanSession(true);
- mco.setAutomaticReconnect(true);
- mco.setKeepAliveInterval(180); // seconds
- synchronized (this) {
- if (BuildConfig.DEBUG && mMqc != null) { throw new AssertionError(); }
- mMqc = mqc;
- }
- mqc.connect(mco, null, mqttal);
- Log.d("CtFwS", "Connect dispatched");
- } catch (MqttException e) {
- Log.e("CtFwS", "Conn Exn", e);
- }
- }
-
- // Must hold strongly since Android only holds weakly once registered.
- private final SharedPreferences.OnSharedPreferenceChangeListener mOSPCL
- = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
- public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
- switch(key) {
- case "server":
- String s = sharedPreferences.getString(key,null);
- if (s != null) { doMqtt(s); }
- break;
+ public void onMqttServerEvent(MainService.LocalBinder b, MainService.MqttServerEvent mse) {
+ switch(mse) {
+ case MSE_CONN: setServerStateText(R.string.mqtt_conn); break;
+ case MSE_DISCONN: setServerStateText(R.string.mqtt_disconn); break;
+ case MSE_SUB: setServerStateText(R.string.mqtt_subbed);
}
}
};
+ private CtFwSDisplayLocal mCdl; // set in onStart
+ private TextView mTvSU; // set in onStart
+ private TextView mTvSS; // set in onStart
+ private void setServerStateText(@StringRes final int resid) {
+ mTvSS.post(new Runnable() {
+ @Override
+ public void run() { mTvSS.setText(resid); }
+ });
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
- String defserver = "tcp://ctfws-mqtt.ietfng.org:1883";
-
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
- mTvSU = (TextView) findViewById(R.id.tv_mqtt_server_uri);
- mTvSS = (TextView) findViewById(R.id.tv_mqtt_state);
-
- CtFwSDisplayLocal mCdl = new CtFwSDisplayLocal(this, new Handler());
- mCgs.registerObserver(mCdl);
-
- mabh.onCreate(mCgs);
-
- mCtfwscbs = new CtFwSCallbacksMQTT(mCgs);
-
- SharedPreferences sp = getPreferences(MODE_PRIVATE);
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
if (sp.getString("server", null) == null) {
sp.edit().putString("server", defserver).apply();
}
throw new AssertionError("Shared Preferences not sticking!");
}
- synchronized(this) {
- sp.registerOnSharedPreferenceChangeListener(mOSPCL);
- doMqtt(sp.getString("server", defserver));
+ mTvSU = (TextView) findViewById(R.id.tv_mqtt_server_uri);
+ mTvSS = (TextView) findViewById(R.id.tv_mqtt_state);
+
+ mCdl = new CtFwSDisplayLocal(this, new Handler());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ bindService(new Intent(this, MainService.class), new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mSrvBinder = (MainService.LocalBinder) service;
+ mSrvBinder.getGameState().registerObserver(mCdl);
+ mSrvBinder.registerObserver(mSrvObs);
+ // Fake an initial server event so we draw some text
+ mSrvObs.onMqttServerEvent(mSrvBinder, mSrvBinder.getServerState());
+ mabh.onStart(MainActivity.this, mSrvBinder);
+ mSrvBinder.connect(false);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mSrvBinder = null;
+ }
+ }, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void onStop() {
+ if (mSrvBinder != null) {
+ mSrvBinder.getGameState().unregisterObserver(mCdl);
+ mSrvBinder.unregisterObserver(mSrvObs);
}
+
+ super.onStop();
}
// Every good application needs an easter egg
// Kick the mqtt layer on a click on the status stuff
public void onclick_connmeta(View v) {
- doMqtt(getPreferences(MODE_PRIVATE).getString("server",null));
+ mSrvBinder.connect(true);
}
// TODO should we be using onClick instead for routing?
case R.id.menu_mqtt :
DialogFragment d =
StringSettingDialogFragment.newInstance(
- R.layout.server_dialog, R.id.server_text, "server");
+ R.layout.server_dialog, R.id.server_text, "server", defserver);
d.show(getSupportFragmentManager(),"serverdialog");
return true;
case R.id.menu_about :
// per build flavor. This will be used when, for example, we kick on Google Play for Wear
// interaction and want to push messages out to the wearable data network.
interface MainActivityBuildHooks {
- void onCreate(CtFwSGameState cgs);
+ void onStart(MainActivity ma, MainService.LocalBinder b);
}
--- /dev/null
+package com.acmetensortoys.ctfwstimer;
+
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
+
+import org.eclipse.paho.android.service.MqttAndroidClient;
+import org.eclipse.paho.android.service.MqttTraceHandler;
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class MainService extends Service {
+ // Android stuff
+ private static final int NOTE_ID_USER = 1;
+ private Handler mHandler; // set in OnCreate
+
+ // The reason we're here!
+ private final CtFwSGameState mCgs
+ = new CtFwSGameState(new CtFwSGameState.TimerProvider() {
+ @Override
+ public long wallMS() {
+ return System.currentTimeMillis();
+ }
+
+ @Override
+ public void postDelay(Runnable r, long delayMS) {
+ mHandler.postDelayed(r, delayMS);
+ }
+
+ @Override
+ public void cancelPost(Runnable r) {
+ mHandler.removeCallbacks(r);
+ }
+ });
+ private CtFwSCallbacksMQTT mCtfwscbs = new CtFwSCallbacksMQTT(mCgs);
+
+ public MainService() {
+ mCgs.registerObserver(mCgsObserver);
+ }
+
+ // MQTT client management
+
+ private MqttAndroidClient mMqc;
+ // Trace MQTT state
+ private final MqttTraceHandler mqttth = new MqttTraceHandler() {
+ @Override
+ public void traceDebug(String tag, String message) {
+ Log.d("CtFwSMqtt:" + tag, message);
+ }
+
+ @Override
+ public void traceError(String tag, String message) {
+ Log.e("CtFwSMqtt:" + tag, message);
+ }
+
+ @Override
+ public void traceException(String tag, String message, Exception e) {
+ Log.e("CtFwSMqtt:" + tag, message, e);
+ }
+ };
+
+ // We'll use this common callback object for our subscriptions below
+ private final IMqttActionListener subal = new IMqttActionListener() {
+ @Override
+ public void onSuccess(IMqttToken asyncActionToken) {
+ Log.d("CtFwS", "Sub OK: " + asyncActionToken);
+ }
+
+ @Override
+ public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+ Log.e("CtFws", "Sub Fail: " + asyncActionToken, exception);
+ setMSE(MqttServerEvent.MSE_CONN);
+ }
+ };
+ // And this handles making our subscriptions for us
+ private final MqttCallbackExtended mqttcb = new MqttCallbackExtended() {
+ @Override
+ public void connectComplete(boolean reconnect, String serverURI) {
+ Log.d("CtFwS", "Conn OK 2 srv=" + serverURI + " reconn=" + reconnect);
+ try {
+ String p = "ctfws/game/";
+ mMqc.subscribe(p+"config" , 2, null, subal, mCtfwscbs.onConfig);
+ mMqc.subscribe(p+"endtime" , 2, null, subal, mCtfwscbs.onEnd);
+ mMqc.subscribe(p+"flags" , 2, null, subal, mCtfwscbs.onFlags);
+ mMqc.subscribe(p+"message" , 2, null, subal, mCtfwscbs.onMessage);
+ mMqc.subscribe(p+"message/player", 2, null, subal, mCtfwscbs.onPlayerMessage);
+ setMSE(MqttServerEvent.MSE_SUB);
+ } catch (MqttException e) {
+ Log.e("CtFwS", "Exn Sub", e);
+ }
+ }
+
+ @Override
+ public void connectionLost(Throwable cause) {
+ Log.d("CtFwS", "Conn Lost: " + cause, cause);
+ setMSE(MqttServerEvent.MSE_DISCONN);
+
+ }
+
+ @Override
+ public void messageArrived(String topic, MqttMessage message) throws Exception {
+ Log.d("CtFwS", "Message(Generic) " + topic + " : '" + message + "'" );
+ }
+
+ @Override
+ public void deliveryComplete(IMqttDeliveryToken token) {
+ // Unused, as we never publish
+ Log.d("CtFwS", "Delivery OK");
+ }
+ };
+ // And this handles yet more about connecting
+ private final IMqttActionListener mqttal = new IMqttActionListener() {
+ @Override
+ public void onSuccess(IMqttToken asyncActionToken) {
+ Log.d("CtFwS", "Conn OK 1");
+ setMSE(MqttServerEvent.MSE_CONN);
+ }
+
+ @Override
+ public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+ Log.e("CtFws", "Conn Fail", exception);
+ setMSE(MqttServerEvent.MSE_DISCONN);
+ }
+ };
+
+ private synchronized void doMqtt(@Nullable String server) {
+ // Hang up on an existing connection, if we have one
+ synchronized (this) {
+ if (mMqc != null) {
+ if (mMqc.isConnected()) {
+ try {
+ mMqc.disconnect();
+ Log.d("CtFwS", "domqtt disconnected");
+ } catch (MqttException me) {
+ Log.e("CtFwS", "domqtt disconn exn", me);
+ }
+ }
+ }
+ mMqc = null;
+ }
+
+ // If we're deliberately disconnecting, tell the service about it. Otherwise, we'll
+ // just keep doing what we're doing until we get some message telling us to do something
+ // else. :)
+ if (server == null) {
+ mCgs.deconfigure();
+ }
+
+ notifyServerChanged(server);
+
+ // If disconnecting is all we were told to do, we're done.
+ if (server == null) {
+ return;
+ }
+
+ // Make our MQTT client and grab callbacks on *everything in sight*
+ //
+ // XXX For reasons beyond my understanding, we have to use a new client ID every time
+ // or we won't resubscribe. I think this is github issue eclipse/paho.mqtt.android#170
+ // but heavens only knows. Whatever, this works for the moment and doesn't leave
+ // stragglers on my server as far as I can tell.
+ MqttAndroidClient mqc = new MqttAndroidClient(this,server, MqttClient.generateClientId());
+ mqc.setCallback(mqttcb);
+ /*
+ // Debugging aid: trace the paho client internals
+ mqc.setTraceCallback(mqttth);
+ mqc.setTraceEnabled(true);
+ */
+
+ // Ahem. Now then. Connect with *more callbacks*, which will fire off our
+ // subscription requests, which of course have *yet more* callbacks, which
+ // react to messages sent to us. Have we lost the thread yet?
+ try {
+ MqttConnectOptions mco = new MqttConnectOptions();
+ mco.setCleanSession(true);
+ mco.setAutomaticReconnect(true);
+ mco.setKeepAliveInterval(180); // seconds
+ synchronized (this) {
+ if (BuildConfig.DEBUG && mMqc != null) { throw new AssertionError(); }
+ mMqc = mqc;
+ }
+ mqc.connect(mco, null, mqttal);
+ Log.d("CtFwS", "Connect dispatched");
+ } catch (MqttException e) {
+ Log.e("CtFwS", "Conn Exn", e);
+ }
+ }
+
+ // Must hold strongly since Android only holds weakly once registered.
+ private final SharedPreferences.OnSharedPreferenceChangeListener mOSPCL
+ = new SharedPreferences.OnSharedPreferenceChangeListener() {
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+ switch(key) { case "server": doMqtt(sharedPreferences.getString(key,null)); break; }
+ }
+ };
+
+ // MQTT Observers
+ public enum MqttServerEvent {
+ MSE_DISCONN,
+ MSE_CONN,
+ MSE_SUB,
+ }
+ private MqttServerEvent mMSE = MqttServerEvent.MSE_DISCONN;
+ public interface Observer {
+ void onMqttServerChanged(LocalBinder b, String sURL);
+ void onMqttServerEvent(LocalBinder b, MqttServerEvent mse);
+ }
+ private final Set<Observer> mObsvs = new HashSet<>();
+ private void setMSE(MqttServerEvent mse) {
+ synchronized(this) {
+ mMSE = mse;
+ for (Observer o : mObsvs) { o.onMqttServerEvent(mBinder, mse); }
+ }
+ }
+ private void notifyServerChanged(String sURL) {
+ synchronized(this) {
+ for (Observer o : mObsvs) { o.onMqttServerChanged(mBinder, sURL); }
+ }
+ }
+
+ // User-facing notification
+ // TODO Move to its own display module?
+ private NotificationCompat.Builder userNoteBuilder;
+ private CtFwSGameState.Observer mCgsObserver = new CtFwSGameState.Observer() {
+ @Override
+ public void onCtFwSConfigure(CtFwSGameState game) { }
+
+ @Override
+ public void onCtFwSNow(CtFwSGameState game, CtFwSGameState.Now now) {
+ userNoteBuilder.setWhen((now.roundEnd+1)*1000);
+ userNoteBuilder.setUsesChronometer(true);
+ if (now.rationale == null || !now.stop) {
+ // game is afoot!
+ userNoteBuilder.setContentTitle(
+ now.rationale == null ? "Game is afoot!" : now.rationale);
+ userNoteBuilder.setContentText(
+ now.round == 0 ? "Setup phase" : ("Round " + now.round));
+ startForeground(NOTE_ID_USER, userNoteBuilder.build());
+ } else {
+ // game no longer afoot
+ stopForeground(true);
+ }
+ }
+
+ @Override
+ public void onCtFwSFlags(CtFwSGameState game) { }
+
+ @Override
+ public void onCtFwSMessage(CtFwSGameState game, List<CtFwSGameState.Msg> msgs) { }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mHandler = new Handler();
+
+
+ userNoteBuilder = new NotificationCompat.Builder(MainService.this)
+ .setSmallIcon(R.drawable.shield1)
+ .setContentIntent(PendingIntent.getActivity(MainService.this, 0,
+ new Intent(MainService.this, MainActivity.class), 0));
+
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this);
+ synchronized(this) {
+ sp.registerOnSharedPreferenceChangeListener(mOSPCL);
+ doMqtt(sp.getString("server", null));
+ }
+ }
+
+ public class LocalBinder extends Binder {
+ CtFwSGameState getGameState() {
+ return mCgs;
+ }
+ MqttServerEvent getServerState() {
+ return mMSE;
+ }
+
+ // It should not be necessary to call this execpt at the beginning or to force a reconnect;
+ // most everything else you might want in a connect method is handled by the
+ // OnSharedPreferenceChangeListener listener above.
+ void connect(boolean force) {
+ if (force || mMSE != MqttServerEvent.MSE_CONN) {
+ doMqtt(PreferenceManager
+ .getDefaultSharedPreferences(MainService.this)
+ .getString("server", null));
+ }
+ }
+ void registerObserver(Observer o) {
+ synchronized(this) { mObsvs.add(o); }
+ }
+ void unregisterObserver(Observer o) {
+ synchronized(this) { mObsvs.remove(o); }
+ }
+ }
+ private LocalBinder mBinder = new LocalBinder();
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Bundle;
+import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.view.LayoutInflater;
private final static String ARG_LRES_IX = "lres"; // layout id
private final static String ARG_VRES_IX = "vres"; // text view id
private final static String ARG_PREF_IX = "pref"; // preference name
+ private final static String ARG_DEFL_IX = "def"; // optional default
private TextView mTv;
- public static StringSettingDialogFragment newInstance(int lres, int vres, String pref) {
+ public static StringSettingDialogFragment newInstance(int lres, int vres, String pref, String def) {
StringSettingDialogFragment ssdf = new StringSettingDialogFragment();
Bundle args = new Bundle();
args.putInt (ARG_LRES_IX, lres);
args.putInt (ARG_VRES_IX, vres);
args.putString(ARG_PREF_IX, pref);
+ if (def != null) { args.putString(ARG_DEFL_IX, def); }
ssdf.setArguments(args);
return ssdf;
}
View v = li.inflate(a.getInt(ARG_LRES_IX), null);
mTv = (TextView)v.findViewById(a.getInt(ARG_VRES_IX));
- final SharedPreferences sp = getActivity().getPreferences(Context.MODE_PRIVATE);
+ final SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
sp.registerOnSharedPreferenceChangeListener(this);
onSharedPreferenceChanged(sp,a.getString(ARG_PREF_IX));
// NOP
}
});
+
+ final String def = a.getString(ARG_DEFL_IX);
+ if (def != null) {
+ adb.setNeutralButton(R.string.dialog_reset, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mTv.post(new Runnable() {
+ @Override
+ public void run() {
+ mTv.setText(def);
+ }
+ });
+ }
+ });
+ }
+
return adb.create();
}
@Override
- public void onSharedPreferenceChanged(SharedPreferences sp, String p) {
+ public void onSharedPreferenceChanged(final SharedPreferences sp, final String p) {
if (p != null && getArguments().getString(ARG_PREF_IX).equals(p)) {
- mTv.setText(sp.getString(p, ""));
+ mTv.post(new Runnable() {
+ @Override
+ public void run() {
+ mTv.setText(sp.getString(p, ""));
+ }
+ });
}
}
}
<string name="ctfws_gameend">Game\nEnd</string>
<string name="ctfws_gamestart">Game\nStart</string>
- <string name="ctfws_jailbreak">Jailbreak %1$d</string>
+ <string name="ctfws_jailbreak">Jailbreak\n%1$d of %2$d</string>
<string name="ctfws_flags">%1$d Flags:</string>
<string name="header_gamestate">Game State:</string>
<string name="dialog_ok">OK</string>
<string name="dialog_cancel">Cancel</string>
+ <string name="dialog_reset">Default</string>
<string name="menutext_about">About</string>
<string name="menutext_mqtt">Set MQTT Server</string>
package com.acmetensortoys.ctfwstimer;
-import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
-
-public class MainActivityBuildHooksImpl implements MainActivityBuildHooks {
+class MainActivityBuildHooksImpl implements MainActivityBuildHooks {
@Override
- public void onCreate(CtFwSGameState cgs) {
- // No-op
+ public void onStart(MainActivity ma, MainService.LocalBinder b) {
+ // NOP
}
-}
\ No newline at end of file
+}