From: Nathaniel Wesley Filardo Date: Tue, 14 Feb 2017 19:38:10 +0000 (-0500) Subject: Move mobile MQTT to Service X-Git-Tag: release-1.2~50 X-Git-Url: https://hydra-www.ietfng.org/gitweb/?a=commitdiff_plain;h=7d478108550996ceab1a68059dbff043ba61dfdf;p=acmetensortoys-ctfws-android Move mobile MQTT to Service While here, add initial Notification There are almost surely bugs; I wrote this when I couldn't sleep --- diff --git a/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java b/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java index 2caf7b9..c6f2ef1 100644 --- a/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java +++ b/lib/src/main/java/com/acmetensortoys/ctfwstimer/lib/CtFwSGameState.java @@ -10,6 +10,17 @@ import java.util.Set; 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; @@ -39,11 +50,7 @@ public class CtFwSGameState { } break; } - if (!isMessageTimeWithin(lastMsgTimestamp)) { - msgs.clear(); - notifyMessages(); - } - notifyConfig(); + notifyConfigEtAl(); } public String toMqttConfigMessage() { if (!configured) { @@ -54,11 +61,11 @@ public class CtFwSGameState { } public void deconfigure() { this.configured = false; - notifyConfig(); + notifyConfigEtAl(); } public void setEndT(long endT) { this.endT = endT; - notifyConfig(); + notifyConfigEtAl(); } public class Now { @@ -66,18 +73,23 @@ public class CtFwSGameState { 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; @@ -173,7 +185,7 @@ public class CtFwSGameState { } 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; @@ -193,9 +205,9 @@ public class CtFwSGameState { public interface Observer { void onCtFwSConfigure(CtFwSGameState game); + void onCtFwSNow(CtFwSGameState game, Now now); void onCtFwSFlags(CtFwSGameState game); void onCtFwSMessage(CtFwSGameState game, List msgs); - } final private Set mObsvs = new HashSet<>(); private void notifyFlags() { @@ -208,10 +220,33 @@ public class CtFwSGameState { 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); } @@ -219,5 +254,4 @@ public class CtFwSGameState { public void unregisterObserver(Observer d) { synchronized(this) { mObsvs.remove(d); } } - } diff --git a/mobile/build.gradle b/mobile/build.gradle index 7a763c9..a8103c5 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -7,7 +7,7 @@ android { applicationId "com.acmetensortoys.ctfwstimer" minSdkVersion 19 targetSdkVersion 25 - versionCode 1 + versionCode 3 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } @@ -31,6 +31,9 @@ android { dimension "play" } } + + // TODO: Should filter out play+debug since it will overflow dex method + // limits (yikes) } repositories { @@ -51,6 +54,7 @@ dependencies { 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' diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index b23d07b..50aa447 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -5,11 +5,11 @@ - - - + + + - + + + \ No newline at end of file diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java index a89eabf..b19e454 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java @@ -27,51 +27,25 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } 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() { @@ -84,7 +58,7 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } else { tv_jb.setText( String.format(mAct.getResources().getString(R.string.ctfws_jailbreak), - now.round)); + now.round, gs.getRounds() - 1)); } } }); diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java index 277ce42..190dc05 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java @@ -1,223 +1,73 @@ 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(); } @@ -225,10 +75,43 @@ public class MainActivity extends AppCompatActivity { 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 @@ -246,7 +129,7 @@ public class MainActivity extends AppCompatActivity { // 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? @@ -256,7 +139,7 @@ public class MainActivity extends AppCompatActivity { 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 : diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooks.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooks.java index 63bea17..8d8098a 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooks.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooks.java @@ -6,5 +6,5 @@ import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState; // 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); } diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java new file mode 100644 index 0000000..467ac8d --- /dev/null +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java @@ -0,0 +1,323 @@ +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 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 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; + } +} diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/StringSettingDialogFragment.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/StringSettingDialogFragment.java index 08b049d..bf17eb5 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/StringSettingDialogFragment.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/StringSettingDialogFragment.java @@ -6,6 +6,7 @@ import android.content.Context; 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; @@ -18,15 +19,17 @@ public class StringSettingDialogFragment extends DialogFragment 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; } @@ -39,7 +42,7 @@ public class StringSettingDialogFragment extends DialogFragment 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)); @@ -56,13 +59,34 @@ public class StringSettingDialogFragment extends DialogFragment // 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, "")); + } + }); } } } diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index e9bf3dc..e53e147 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ Game\nEnd Game\nStart - Jailbreak %1$d + Jailbreak\n%1$d of %2$d %1$d Flags: Game State: @@ -12,6 +12,7 @@ OK Cancel + Default About Set MQTT Server diff --git a/mobile/src/noplay/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooksImpl.java b/mobile/src/noplay/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooksImpl.java index 88b0d04..6fe3d90 100644 --- a/mobile/src/noplay/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooksImpl.java +++ b/mobile/src/noplay/java/com/acmetensortoys/ctfwstimer/MainActivityBuildHooksImpl.java @@ -1,10 +1,8 @@ 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 +}