package com.acmetensortoys.ctfwstimer.lib;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Scanner;
+import java.util.Set;
public class CtFwSGameState {
- public boolean configured;
- public long startT; // NTP seconds for game start
- public int setupD;
- public int rounds;
- public int roundD;
- public long endT = 0; // NTP seconds for game end (if >= startT)
- public int flagsTotal;
- public boolean flagsVisible = false;
- public int flagsRed = 0;
- public int flagsYel = 0;
+ // Game time
- public void setFlags(boolean visible) {
- flagsVisible = visible;
- }
- public void setFlags(int red, int yel) {
- flagsRed = red; flagsYel = yel;
- }
+ private boolean configured = false;
+ private long startT; // NTP seconds for game start
+ private int setupD;
+ private int rounds;
+ private int roundD;
+ private long endT = 0; // NTP seconds for game end (if >= startT)
- public void mqttConfigMessage(String st) {
+ public void fromMqttConfigMessage(String st) {
String tm = st.trim();
switch (tm) {
case "none":
}
break;
}
+ if (!isMessageTimeWithin(lastMsgTimestamp)) {
+ msgs.clear();
+ notifyMessages();
+ }
+ notifyConfig();
}
- public void mqttFlagsMessage(String st) {
- String tm = st.trim();
- switch(tm) {
- case "?":
- this.setFlags(false);
- break;
- default:
- Scanner s = new Scanner(tm);
- try {
- this.setFlags(true);
- this.setFlags(s.nextInt(),s.nextInt());
- } catch (NumberFormatException e) {
- this.setFlags(false);
- }
+ public String toMqttConfigMessage() {
+ if (!configured) {
+ return "none";
}
+
+ return String.format(Locale.ROOT, "%d %d %d %d %d", startT, setupD, rounds, roundD, flagsTotal);
+ }
+ public void deconfigure() {
+ this.configured = false;
+ notifyConfig();
+ }
+ public void setEndT(long endT) {
+ this.endT = endT;
+ notifyConfig();
}
public class Now {
public String rationale = null; // null if game is in play, otherwise other fields invalid
+ public boolean stop = false;
public int round = 0; // 0 for setup
public long roundStart = 0, roundEnd = 0; // NTP seconds
- public boolean stop = false;
}
public Now getNow(long now) {
Now res = new Now();
res.stop = true;
} else if (now <= startT) {
res.rationale = "Start time in the future!";
+ res.roundStart = startT;
}
if (res.rationale != null) {
return res;
res.round += 1;
return res;
}
+ public boolean isConfigured(){
+ return configured;
+ }
+ public long getStartT() { return startT; }
+ public long getFirstRoundStartT() { return startT + setupD; }
+ public int getRounds() { return rounds; }
+ public int getComputedGameDuration() { return rounds * roundD ; }
+
+ // Leaves off the natural endT comparison so that messages can be posted after the
+ // game ends and still count as part of this one (i.e. still be displayed).
+ private boolean isMessageTimeWithin(long time) {
+ return !configured || time >= startT;
+ }
+
+ // Game score
+
+ public int flagsTotal;
+ public boolean flagsVisible = false;
+ public int flagsRed = 0;
+ public int flagsYel = 0;
+
+ public void fromMqttFlagsMessage(String st) {
+ String tm = st.trim();
+ switch(tm) {
+ case "?":
+ flagsVisible = false;
+ break;
+ default:
+ Scanner s = new Scanner(tm);
+ try {
+ flagsVisible = true;
+ int red = s.nextInt();
+ int yel = s.nextInt();
+ flagsRed = red;
+ flagsYel = yel;
+ } catch (NumberFormatException e) {
+ flagsVisible = false;
+ }
+ }
+ notifyFlags();
+ }
+ public String toMqttFlagsMessage() {
+ if (!configured || !flagsVisible) {
+ return "?";
+ }
+
+ return String.format(Locale.ROOT, "%d %d", flagsRed, flagsYel);
+ }
+
+ // Informative messages handling
+
+ public class Msg {
+ public long when;
+ public String msg;
+
+ Msg(long when, String msg) {
+ this.when = when;
+ this.msg = msg;
+ }
+ }
+ private List<Msg> msgs = new ArrayList<>();
+ private long lastMsgTimestamp;
+
+ public void onNewMessage(String str) {
+ Scanner s = new Scanner(str);
+ long t;
+
+ try {
+ t = s.nextLong();
+ } 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;
+ msgs.add(new Msg(lastMsgTimestamp, str));
+ notifyMessages();
+ return;
+ }
+
+ // If there is no configuration, assume the message is new enough
+ // If there *is* a configuration, check the time.
+ if (isMessageTimeWithin(t) && (lastMsgTimestamp <= t)) {
+ s.useDelimiter("\\z");
+ lastMsgTimestamp = t;
+ msgs.add(new Msg(lastMsgTimestamp, s.next().trim()));
+ notifyMessages();
+ }
+ }
+
+ // Observer interface
+
+ public interface Observer {
+ void onCtFwSConfigure(CtFwSGameState game);
+ void onCtFwSFlags(CtFwSGameState game);
+ void onCtFwSMessage(CtFwSGameState game, List<Msg> msgs);
+
+ }
+ final private Set<Observer> mObsvs = new HashSet<>();
+ private void notifyFlags() {
+ synchronized(this) {
+ for (Observer o : mObsvs) { o.onCtFwSFlags(this); }
+ }
+ }
+ private void notifyMessages() {
+ synchronized(this) {
+ for (Observer o : mObsvs) { o.onCtFwSMessage(this, msgs); }
+ }
+ }
+ private void notifyConfig() {
+ synchronized(this) {
+ for (Observer o : mObsvs) { o.onCtFwSConfigure(this); }
+ }
+ }
+ public void registerObserver(Observer d) {
+ synchronized(this) { mObsvs.add(d); }
+ }
+ public void unregisterObserver(Observer d) {
+ synchronized(this) { mObsvs.remove(d); }
+ }
+
}
+++ /dev/null
-package com.acmetensortoys.ctfwstimer.lib;
-
-import java.util.NoSuchElementException;
-import java.util.Scanner;
-
-public class CtFwSMessageFilter {
- final private CtFwSGameState mCgs;
- public CtFwSMessageFilter(CtFwSGameState cgs) {
- mCgs = cgs;
- }
-
- public class Msg {
- public long when;
- public String msg;
-
- public Msg(long when, String msg) {
- this.when = when;
- this.msg = msg;
- }
- }
-
- private long lastMsgTimestamp = 0;
-
- public Msg filter(String str) {
- Scanner s = new Scanner(str);
- long t;
- try {
- t = s.nextLong();
- } 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;
- return new Msg(lastMsgTimestamp, str);
- }
-
- // If there is no configuration, assume the message is new enough
- // If there *is* a configuration, check the time.
- if ((!mCgs.configured || t >= mCgs.startT) && (lastMsgTimestamp <= t)) {
- s.useDelimiter("\\z");
- lastMsgTimestamp = t;
- return new Msg(lastMsgTimestamp, s.next().trim());
- }
-
- return null;
- }
-}
android {
compileSdkVersion 25
- buildToolsVersion "24.0.3"
+ buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.acmetensortoys.ctfwstimer"
minSdkVersion 19
- targetSdkVersion 24
+ targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
- minifyEnabled false
+ minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+
+ flavorDimensions "play"
+ productFlavors {
+ noplay {
+ dimension "play"
+ }
+ play {
+ dimension "play"
+ }
+ }
}
repositories {
maven {
- url "https://repo.eclipse.org/content/repositories/paho-releases/"
+ url "https://repo.eclipse.org/content/repositories/paho-snapshots/"
}
}
compile project(":lib")
- compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
- exclude module: 'support-v4'
- }
compile 'com.android.support:appcompat-v7:25.1.1'
- compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
compile 'com.android.support:support-v4:25.1.1'
+
+ playCompile 'com.google.android.gms:play-services:10.0.1'
+
+ compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.1-SNAPSHOT'
+ compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.2-SNAPSHOT') {
+ exclude module: 'support-v4'
+ }
+
testCompile 'junit:junit:4.12'
}
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
-#}
-
-# Work around some Paho packaging warnings that manifest as errors.
-# See http://github.com/eclipse/paho.mqtt.android/issues/79
--keepattributes InnerClasses
--dontoptimize
\ No newline at end of file
+#}
\ No newline at end of file
import android.util.Log;
import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
-import com.acmetensortoys.ctfwstimer.lib.CtFwSMessageFilter;
import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
import org.eclipse.paho.client.mqttv3.MqttMessage;
-import java.util.NoSuchElementException;
-import java.util.Scanner;
-
class CtFwSCallbacksMQTT {
- final private CtFwSDisplay mCdl;
final private CtFwSGameState mCgs;
- final private CtFwSMessageFilter mCmf;
- CtFwSCallbacksMQTT(CtFwSDisplay cdl, CtFwSGameState cgs) {
- mCdl = cdl;
+ CtFwSCallbacksMQTT(CtFwSGameState cgs) {
mCgs = cgs;
- mCmf = new CtFwSMessageFilter(mCgs);
}
IMqttMessageListener onConfig = new IMqttMessageListener() {
public void messageArrived(String topic, MqttMessage message) throws Exception {
String tm = message.toString().trim();
Log.d("CtFwS", "Message(Config): " + tm);
- mCgs.mqttConfigMessage(tm);
- mCdl.notifyGameState();
+ mCgs.fromMqttConfigMessage(tm);
}
};
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
Log.d("CtFwS", "Message(End): " + message);
+ long endT;
try {
- mCgs.endT = Long.parseLong(message.toString());
+ endT = Long.parseLong(message.toString());
} catch (NumberFormatException e) {
- mCgs.endT = 0;
+ endT = 0;
}
- mCdl.notifyGameState();
+ mCgs.setEndT(endT);
}
};
public void messageArrived(String topic, MqttMessage message) throws Exception {
String tm = message.toString().trim();
Log.d("CtFwS", "Message(Flags): " + tm);
- mCgs.mqttFlagsMessage(tm);
- mCdl.notifyFlags();
+ mCgs.fromMqttFlagsMessage(tm);
}
};
- private void onMessageCommon(String str) {
- CtFwSMessageFilter.Msg m = mCmf.filter(str);
- if (m != null) {
- mCdl.notifyMessage(m.when, m.msg);
- }
- }
-
IMqttMessageListener onMessage = new IMqttMessageListener() {
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
String str = message.toString();
Log.d("CtFwS", "Message(Broadcast): " + str);
- onMessageCommon(str);
+ mCgs.onNewMessage(str);
}
};
public void messageArrived(String topic, MqttMessage message) throws Exception {
String str = message.toString();
Log.d("CtFwS", "Message(Players): " + str);
- onMessageCommon(str);
+ mCgs.onNewMessage(str);
}
};
}
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.Log;
+import android.view.View;
import android.widget.Chronometer;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
-// TODO nwf is bad at UI design; someone who isn't him should improve this
+import java.util.List;
+
+import static android.view.View.INVISIBLE;
-class CtFwSDisplay {
+// TODO nwf is bad at UI design; someone who isn't him should improve this
+class CtFwSDisplayLocal implements CtFwSGameState.Observer {
final private Activity mAct;
final private Handler mHandler;
- final private CtFwSGameState mCgs;
- private long lastMsgTimeMS = 0;
-
- CtFwSDisplay(Activity a, Handler h, CtFwSGameState cgs) {
+ CtFwSDisplayLocal(Activity a, Handler h) {
mAct = a;
mHandler = h;
- mCgs = cgs;
}
- final private Runnable mProber = new Runnable() {
- @Override
- public void run() {
- notifyGameState();
- }
- };
+ private Runnable mProber;
- void notifyGameState() {
+ @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 = mCgs.getNow(nowMS / 1000);
+ final CtFwSGameState.Now now = gs.getNow(nowMS / 1000);
+ final Runnable prober = new Runnable() {
+ @Override
+ public void run() {
+ onCtFwSConfigure(gs);
+ }
+ };
+
Log.d("CtFwS", "Display game state; nowMS=" + nowMS + " r=" + now.round + " rs=" + now.roundStart + " re=" + now.roundEnd);
doReset();
- mHandler.removeCallbacks(mProber);
+ if (mProber != null) {
+ mHandler.removeCallbacks(mProber);
+ }
if (!now.stop) {
- mHandler.postDelayed(mProber, mCgs.startT*1000 - nowMS);
+ mProber = prober;
+ mHandler.postDelayed(mProber, now.roundStart*1000 - nowMS);
}
return;
// Otherwise, it's game on!
- // Clear the mesage log if it looks like it's a new game in play
- if (lastMsgTimeMS < mCgs.startT * 1000) {
- clearMsgs();
- }
-
+ // 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);
{
public void run() {
if (now.round == 0) {
tv_jb.setText(R.string.ctfws_gamestart);
- } else if (now.round == mCgs.rounds) {
+ } else if (now.round == gs.getRounds()) {
tv_jb.setText(R.string.ctfws_gameend);
} else {
tv_jb.setText(
@Override
public void run() {
pb_jb.setIndeterminate(false);
- if (now.round == 0) {
- pb_jb.setMax(mCgs.setupD - 1);
- } else {
- pb_jb.setMax(mCgs.roundD - 1);
- }
+ pb_jb.setMax((int)(now.roundEnd - now.roundStart));
pb_jb.setProgress(0);
}
});
ch_jb.post(new Runnable() {
@Override
public void run() {
- ch_jb.setBase(now.roundEnd * 1000 - tbcf);
+ ch_jb.setBase((now.roundEnd + 1) * 1000 - tbcf);
ch_jb.setOnChronometerTickListener(new Chronometer.OnChronometerTickListener() {
@Override
public void onChronometerTick(Chronometer c) {
- pb_jb.setProgress((int)(now.roundEnd - System.currentTimeMillis()/1000) - 1);
+ pb_jb.setProgress((int)(now.roundEnd - System.currentTimeMillis()/1000));
}
});
ch_jb.start();
@Override
public void run() {
pb_gp.setIndeterminate(false);
- pb_gp.setMax(mCgs.rounds * mCgs.roundD - 1);
+ pb_gp.setMax(gs.getComputedGameDuration());
pb_gp.setProgress(0);
}
});
ch_gp.post(new Runnable() {
@Override
public void run() {
- ch_gp.setBase((mCgs.startT + mCgs.setupD) * 1000 - tbcf);
+ ch_gp.setBase(gs.getFirstRoundStartT() * 1000 - tbcf);
ch_gp.setOnChronometerTickListener(new Chronometer.OnChronometerTickListener() {
@Override
public void onChronometerTick(Chronometer c) {
pb_gp.setProgress((int)(System.currentTimeMillis()/1000
- - mCgs.startT - mCgs.setupD));
+ - gs.getFirstRoundStartT()));
}
});
+ ch_gp.setVisibility(View.VISIBLE);
ch_gp.start();
}
});
pb_gp.post(new Runnable() {
@Override
public void run() {
- pb_gp.setIndeterminate(false);
- pb_gp.setMax(mCgs.rounds * mCgs.roundD - 1);
- pb_gp.setProgress(0);
+ pb_gp.setIndeterminate(true);
}
});
ch_gp.post(new Runnable() {
@Override
public void run() {
- ch_gp.setBase(nowMS - tbcf);
- ch_gp.setOnChronometerTickListener(null);
ch_gp.stop();
+ ch_gp.setVisibility(INVISIBLE);
}
});
}
@Override
public void run() {
tv_flags.setText(
- String.format(mAct.getResources().getString(R.string.ctfws_flags),
- mCgs.flagsTotal));
+ String.format(mAct.getResources().getString(R.string.ctfws_flags), gs.flagsTotal));
}
});
}
ch.post(new Runnable() {
@Override
public void run() {
- ch.setOnChronometerTickListener(null);
- ch.setBase(SystemClock.elapsedRealtime());
ch.stop();
- }
- });
+ ch.setVisibility(View.INVISIBLE);
+ }});
}
{
final ProgressBar pb = (ProgressBar) (mAct.findViewById(R.id.pb_jailbreak));
}
}
- void notifyFlags() {
+ @Override
+ public void onCtFwSFlags(CtFwSGameState gs) {
// TODO: This stinks
final StringBuffer sb = new StringBuffer();
- if (mCgs.configured) {
- if (mCgs.flagsVisible) {
+ if (gs.isConfigured()) {
+ if (gs.flagsVisible) {
sb.append("r=");
- sb.append(mCgs.flagsRed);
+ sb.append(gs.flagsRed);
sb.append(" y=");
- sb.append(mCgs.flagsYel);
+ sb.append(gs.flagsYel);
} else {
sb.append("r=? y=?");
}
});
}
- void clearMsgs() {
- final TextView msgs = (TextView) (mAct.findViewById(R.id.msgs));
- msgs.post(new Runnable() {
- @Override
- public void run() {
- msgs.setText("");
- }
- });
- }
+ @Override
+ public void onCtFwSMessage(CtFwSGameState gs, List<CtFwSGameState.Msg> msgs) {
+ final TextView msgstv = (TextView)(mAct.findViewById(R.id.msgs));
+ int s = msgs.size();
- void notifyMessage(long ts, String m) {
- final StringBuffer sb = new StringBuffer();
- long td = (ts == 0) ? 0 : (mCgs.configured) ? ts - mCgs.startT : 0;
- sb.append(DateUtils.formatElapsedTime(td));
- sb.append(": ");
- sb.append(m);
- sb.append("\n");
+ if (s == 0) {
+ msgstv.post(new Runnable() {
+ @Override
+ public void run() {
+ msgstv.setText("");
+ }
+ });
+ } else {
- final TextView msgs = (TextView)(mAct.findViewById(R.id.msgs));
- msgs.post(new Runnable() {
- @Override
- public void run() {
- msgs.append(sb);
- }
- });
+ CtFwSGameState.Msg m = msgs.get(s - 1);
+
+ long td = (m.when == 0) ? 0 : (gs.isConfigured()) ? m.when - gs.getStartT() : 0;
+
+ final StringBuffer sb = new StringBuffer();
+ sb.append(DateUtils.formatElapsedTime(td));
+ sb.append(": ");
+ sb.append(m.msg);
+ sb.append("\n");
+
+ msgstv.post(new Runnable() {
+ @Override
+ public void run() {
+ msgstv.append(sb);
+ }
+ });
+ }
}
}
import android.widget.TextView;
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.MqttClientPersistence;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
private MqttAndroidClient mMqc;
private final CtFwSGameState mCgs = new CtFwSGameState();
- private CtFwSDisplay mCdl; // set in onCreate
private CtFwSCallbacksMQTT mCtfwscbs ; // set in onCreate
+ private MainActivityBuildHooks mabh = new MainActivityBuildHooksImpl();
+
private TextView mTvSU; // set in onCreate
private TextView mTvSS; // set in onCreate
private void setServerStateText(@StringRes final int resid) {
public void onSuccess(IMqttToken asyncActionToken) {
Log.d("CtFwS", "Conn OK 1");
setServerStateText(R.string.mqtt_conn);
- mCdl.clearMsgs();
}
@Override
public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
Log.e("CtFws", "Conn Fail", exception);
setServerStateText(R.string.mqtt_disconn);
- mCdl.clearMsgs();
}
};
}
}
mMqc = null;
- mCgs.configured = false;
- mCdl.notifyGameState();
+ mCgs.deconfigure();
}
// If that's all we were told to do, we're done
@Override
protected void onCreate(Bundle savedInstanceState) {
- String defserver = "tcp://nwf1.xen.prgmr.com:1883";
+ 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);
- mCdl = new CtFwSDisplay(this, new Handler(), mCgs);
- mCtfwscbs = new CtFwSCallbacksMQTT(mCdl, mCgs);
+ CtFwSDisplayLocal mCdl = new CtFwSDisplayLocal(this, new Handler());
+ mCgs.registerObserver(mCdl);
+
+ mabh.onCreate(mCgs);
+
+ mCtfwscbs = new CtFwSCallbacksMQTT(mCgs);
SharedPreferences sp = getPreferences(MODE_PRIVATE);
if (sp.getString("server", null) == null) {
--- /dev/null
+package com.acmetensortoys.ctfwstimer;
+
+import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
+
+// The MainActivity expects a "MainActivityBuildHooksImpl" class that ascribes to this interface
+// 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);
+}
<Chronometer
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:id="@+id/ch_gameProgress" />
+ android:id="@+id/ch_gameProgress"
+ android:visibility="invisible" />
</TableRow>
--- /dev/null
+package com.acmetensortoys.ctfwstimer;
+
+import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
+
+public class MainActivityBuildHooksImpl implements MainActivityBuildHooks {
+ @Override
+ public void onCreate(CtFwSGameState cgs) {
+ // No-op
+ }
+}
\ No newline at end of file