From 8e9497c562767896962301b1f6252ba027b8cdac Mon Sep 17 00:00:00 2001 From: Nathaniel Wesley Filardo Date: Fri, 10 Nov 2017 18:03:50 -0500 Subject: [PATCH] A bunch of changes Add stun timers Try to fix up spurious message delivery (working around Paho issues) Bump version to 1.0.1 --- build.gradle | 2 +- mobile/build.gradle | 6 +-- .../ctfwstimer/CtFwSCallbacksMQTT.java | 6 ++- .../ctfwstimer/CtFwSDisplayLocal.java | 25 ++++++--- .../ctfwstimer/MainActivity.java | 15 ++++-- .../ctfwstimer/MainService.java | 53 +++++++++++++++---- .../ctfwstimer/MainServiceNotification.java | 53 ++++++++++++------- mobile/src/main/res/layout/activity_main.xml | 49 +++++++++-------- mobile/src/main/res/menu/mainmenu.xml | 27 +++++++--- mobile/src/main/res/values/strings.xml | 8 ++- mobile/src/main/res/xml/preferences.xml | 20 +++++++ wear/build.gradle | 2 +- 12 files changed, 189 insertions(+), 77 deletions(-) diff --git a/build.gradle b/build.gradle index c2eea8e..c33a638 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.android.tools.build:gradle:3.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/mobile/build.gradle b/mobile/build.gradle index 370b215..051921f 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -2,13 +2,13 @@ apply plugin: 'com.android.application' android { compileSdkVersion 25 - buildToolsVersion "25.0.3" + buildToolsVersion '26.0.2' defaultConfig { applicationId "com.acmetensortoys.ctfwstimer" minSdkVersion 16 targetSdkVersion 25 - versionCode 6 - versionName "1.0" + versionCode 7 + versionName "1.0.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java index 25bf3ff..94ccc98 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java @@ -8,12 +8,16 @@ import org.eclipse.paho.client.mqttv3.IMqttMessageListener; import org.eclipse.paho.client.mqttv3.MqttMessage; class CtFwSCallbacksMQTT { - final private CtFwSGameState mCgs; + private CtFwSGameState mCgs; CtFwSCallbacksMQTT(CtFwSGameState cgs) { mCgs = cgs; } + final public void dispose() { + mCgs = null; + } + final IMqttMessageListener onConfig = new IMqttMessageListener() { @Override public void messageArrived(String topic, MqttMessage message) throws Exception { diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java index f2f9f86..81c0356 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java @@ -44,7 +44,7 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } private void wireTimer(int vid, final StunTimer st) { - ((Button)mAct.findViewById(vid)) + mAct.findViewById(vid) .setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -160,6 +160,7 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { pb_jb.setProgress((int) (now.roundEnd - System.currentTimeMillis() / 1000)); } }); + ch_jb.setVisibility(View.VISIBLE); ch_jb.start(); } }); @@ -235,6 +236,7 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { ch.setOnChronometerTickListener(null); ch.setBase(SystemClock.elapsedRealtime()); ch.stop(); + ch.setVisibility(View.INVISIBLE); } }); } @@ -344,12 +346,20 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { resumeTimer(st, wallStart + st.ms); } + private void hideTimer(final StunTimer st) { + st.ch.setOnChronometerTickListener(null); + st.ch.setVisibility(View.INVISIBLE); + st.pb.setVisibility(View.INVISIBLE); + } + private void resumeTimer(final StunTimer st, final long wallEnd) { + Log.d("CtFwS", "Timer start: " + st.ms); + st.wallEndMS = wallEnd; + final long nowWall = System.currentTimeMillis(); - if (nowWall < wallEnd) { - st.ch.setOnChronometerTickListener(null); - st.ch.setVisibility(View.INVISIBLE); - st.pb.setVisibility(View.INVISIBLE); + if (wallEnd < nowWall) { + Log.d("CtFwS", "Timer finished in past"); + hideTimer(st); return; } @@ -362,10 +372,13 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { public void onChronometerTick(Chronometer chronometer) { final long nowAbsCB = System.currentTimeMillis(); st.pb.setProgress((int) (wallEnd - nowAbsCB)); + if (wallEnd < nowAbsCB) { + hideTimer(st); + } } }); - st.pb.setProgress((int) (wallEnd - nowWall)); + st.pb.setMax((int) (wallEnd - nowWall)); st.ch.start(); st.ch.setVisibility(View.VISIBLE); st.pb.setVisibility(View.VISIBLE); diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java index 677ba89..f6597f4 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.pm.ActivityInfo; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.StringRes; @@ -18,8 +19,6 @@ import android.view.MenuItem; import android.view.View; import android.widget.TextView; -// TODO There should be an I've-been-stunned timer, too. - public class MainActivity extends AppCompatActivity { private final MainActivityBuildHooks mabh = new MainActivityBuildHooksImpl(); @@ -64,6 +63,9 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + // TODO: should probably look better in landscape, too. + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); if (sp.getString("server", null) == null) { sp.edit().putString("server", getString(R.string.server_default)).apply(); @@ -165,7 +167,9 @@ public class MainActivity extends AppCompatActivity { // Kick the mqtt layer on a click on the status stuff public void onclick_connmeta(@SuppressWarnings("UnusedParameters") View v) { - mSrvBinder.connect(true); + if (mSrvBinder != null) { + mSrvBinder.connect(true); + } } // TODO should we be using onClick instead for routing? @@ -174,6 +178,11 @@ public class MainActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem mi) { switch(mi.getItemId()) { + case R.id.menu_reconn: + if (mSrvBinder != null) { + mSrvBinder.connect(true); + } + return true; case R.id.menu_prf : startActivity(new Intent(this, SettingsActivity.class)); return true; diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java index 6501af2..e4cfcb1 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java @@ -51,7 +51,6 @@ public class MainService extends Service { mHandler.removeCallbacks(r); } }); - private final CtFwSCallbacksMQTT mCtfwscbs = new CtFwSCallbacksMQTT(mCgs); @SuppressWarnings({"FieldCanBeLocal", "unused"}) private MainServiceNotification mMsn; // set in onCreate @@ -89,14 +88,17 @@ public class MainService extends Service { @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() { + private class MyMQTTCallbacks implements MqttCallbackExtended { + public CtFwSCallbacksMQTT mCtfwscbs; + @Override public void connectComplete(boolean reconnect, String serverURI) { Log.d("CtFwS", "Conn OK 2 srv=" + serverURI + " reconn=" + reconnect); + mCtfwscbs = new CtFwSCallbacksMQTT(mCgs); + String p = "ctfws/game/"; try { mMqc.subscribe(p + "config", 2, null, subal, mCtfwscbs.onConfig); @@ -113,6 +115,8 @@ public class MainService extends Service { @Override public void connectionLost(Throwable cause) { Log.d("CtFwS", "Conn Lost: " + cause, cause); + mCtfwscbs.dispose(); + mCtfwscbs = null; setMSE(MqttServerEvent.MSE_DISCONN); } @@ -127,7 +131,9 @@ public class MainService extends Service { Log.d("CtFwS", "Delivery OK"); } }; - // And this handles yet more about connecting + private final MyMQTTCallbacks mqttcb = new MyMQTTCallbacks(); + + // And this handles yet more about connecting private final IMqttActionListener mqttal = new IMqttActionListener() { @Override public void onSuccess(IMqttToken asyncActionToken) { @@ -138,7 +144,8 @@ public class MainService extends Service { } else { Log.d("Service", "IS STALE CONN"); try { - c.disconnect().waitForCompletion(); + // TODO Should we waitforcompletion here? + c.disconnect(); } catch (MqttException me) { // Drop it, we've already dropped the client handle } @@ -163,6 +170,19 @@ public class MainService extends Service { if (mMqc != null) { mMqc.setCallback(null); + // Observationally, it looks like .close() below isn't enough! Deliberately + // fling unsubscriptions at the server. + try { + String p = "ctfws/game/"; + mMqc.unsubscribe(new String[]{ + p + "config", p + "endtime", p + "flags", + p + "message", p + "message/player" + }); + } catch (MqttException me) { + Log.d("Service", "domqtt discon unsub exn"); + // *&@#&^*#@#&@#&@# + } + // TODO: This is *really* annoying; we might leak a connection here because // .disconnect() is so @#*@&#*@^#*&@^ asynchronous it hurts. There appears // to be no way to force its hand, and adding .waitforcompletion() here just @@ -176,10 +196,21 @@ public class MainService extends Service { // *&@#&^*#@#&@#&@# } mMqc.unregisterResources(); + } else { Log.d("Service", "domqtt no client"); } + // At this point, prevent the client we just shot down from making any further changes + // to the state machine. This is a little grody, since you'd think we'd just have done + // just that, what with all the disconnecting and the closing and unregistering of + // callbacks, but Paho is a steaming pile of Enterprise Code and apparently loves to + // hold on to things. So we are about to force a lot of NPEs by nulling out our callback + // holder's reference to the game state. + if (mqttcb.mCtfwscbs != null) { + mqttcb.mCtfwscbs.dispose(); + } + // 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. :) @@ -213,7 +244,7 @@ public class MainService extends Service { MqttConnectOptions mco = new MqttConnectOptions(); mco.setCleanSession(true); mco.setAutomaticReconnect(true); - mco.setKeepAliveInterval(180); // seconds + mco.setKeepAliveInterval(10); // seconds try { mMqc.connect(mco, null, mqttal); } catch (MqttException e) { @@ -235,9 +266,9 @@ public class MainService extends Service { // MQTT Observers public enum MqttServerEvent { - MSE_DISCONN, - MSE_CONN, - MSE_SUB, + MSE_DISCONN, /* No active connection */ + MSE_CONN, /* Connected, but not subscribed */ + MSE_SUB, /* Subscriptions have been registered */ } private MqttServerEvent mMSE = MqttServerEvent.MSE_DISCONN; public interface Observer { @@ -282,7 +313,7 @@ public class MainService extends Service { } } void registerObserver(Observer o) { - synchronized(this) { + synchronized(MainService.this) { if (mObsvs.add(o)) { // Fire off synthetic deltas to bring the observer up to date. if (mMqc == null) { @@ -295,7 +326,7 @@ public class MainService extends Service { } } void unregisterObserver(Observer o) { - synchronized(this) { mObsvs.remove(o); } + synchronized(MainService.this) { mObsvs.remove(o); } } } private final LocalBinder mBinder = new LocalBinder(); diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java index 5e025ec..f9bd49d 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java @@ -6,6 +6,8 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.res.Resources; +import android.media.RingtoneManager; +import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; @@ -21,7 +23,7 @@ class MainServiceNotification { private long lastVibrateTime; - private enum VibrationSource { NONE, BREAK, FLAG, MESG } + private enum NotificationSource { NONE, BREAK, FLAG, MESG } private enum LastContentTextSource { NONE, FLAG, MESG } private LastContentTextSource lastContextTextSource = LastContentTextSource.NONE; @@ -69,7 +71,7 @@ class MainServiceNotification { userNoteBuilder.setSubText(now.rationale); } - vibrate(VibrationSource.BREAK); + notifyUserSomehow(NotificationSource.BREAK); ensureNotification(); } else { // game no longer afoot @@ -85,7 +87,7 @@ class MainServiceNotification { if (game.flagsVisible && ((lastContextTextSource == LastContentTextSource.FLAG) || (game.flagsRed + game.flagsYel > 0))) { - vibrate(VibrationSource.FLAG); + notifyUserSomehow(NotificationSource.FLAG); lastContextTextSource = LastContentTextSource.FLAG; userNoteBuilder.setContentText( String.format(mService.getResources().getString(R.string.notify_flags), @@ -99,7 +101,7 @@ class MainServiceNotification { // Only do anything if we aren't clearing the message list int s = msgs.size(); if (s != 0) { - vibrate(VibrationSource.MESG); + notifyUserSomehow(NotificationSource.MESG); lastContextTextSource = LastContentTextSource.MESG; userNoteBuilder.setContentText(msgs.get(s - 1).msg); refreshNotification(); @@ -109,34 +111,42 @@ class MainServiceNotification { } // TODO make all of these configurable? - private final long VIBRATE_SUPPRESS_THRESHOLD = 5000; // suppress rapid-fire buzzing + private final long NOTIFY_SUPPRESS_THRESHOLD = 5000; // suppress rapid-fire buzzing + private final long[] VIBRATE_PATTERN_NOW = {0, 100, 100, 300, 100, 300, 100, 300}; // 'J' = .--- private final long[] VIBRATE_PATTERN_FLAG = {0, 100, 100, 100, 100, 300, 100, 100}; // 'F' = ..-. private final long[] VIBRATE_PATTERN_MSG = {0, 300, 100, 300}; // 'M' = -- - private void vibrate(VibrationSource vs) { + + private void notifyUserSomehow(NotificationSource vs) { long now = System.currentTimeMillis(); // Clobber the vibration request if we probably recently did such a thing - if ((now - lastVibrateTime < VIBRATE_SUPPRESS_THRESHOLD)) { - vs = VibrationSource.NONE; + if ((now - lastVibrateTime < NOTIFY_SUPPRESS_THRESHOLD)) { + vs = NotificationSource.NONE; } - String pref; - long[] pattern; + String vpref; + long[] vpattern; + + String spref; + Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); switch(vs) { case BREAK: - pref = "prf_vibr_jb"; - pattern = VIBRATE_PATTERN_NOW; + vpref = "prf_vibr_jb"; + spref = "prf_sound_jb"; + vpattern = VIBRATE_PATTERN_NOW; break; case FLAG: - pref = "prf_vibr_flag"; - pattern = VIBRATE_PATTERN_FLAG; + vpref = "prf_vibr_flag"; + spref = "prf_sound_flag"; + vpattern = VIBRATE_PATTERN_FLAG; break; case MESG: - pref = "prf_vibr_mesg"; - pattern = VIBRATE_PATTERN_MSG; + vpref = "prf_vibr_mesg"; + spref = "prf_sound_mesg"; + vpattern = VIBRATE_PATTERN_MSG; break; case NONE: default: @@ -147,13 +157,20 @@ class MainServiceNotification { // Cam: default value is "false" because we really don't want to be vibrating if we // accidentally lose our preferences somehow if (PreferenceManager.getDefaultSharedPreferences(mService.getBaseContext()) - .getBoolean(pref, false)) { - userNoteBuilder.setVibrate(pattern); + .getBoolean(vpref, false)) { + userNoteBuilder.setVibrate(vpattern); lastVibrateTime = now; } else { userNoteBuilder.setVibrate(null); } + + if (PreferenceManager.getDefaultSharedPreferences(mService.getBaseContext()) + .getBoolean(spref, false)) { + userNoteBuilder.setSound(soundUri); + } else { + userNoteBuilder.setSound(null); + } } private ServiceConnection userNoteSC; diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml index 4f5d77b..d1072a7 100644 --- a/mobile/src/main/res/layout/activity_main.xml +++ b/mobile/src/main/res/layout/activity_main.xml @@ -18,7 +18,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:clickable="true" android:onClick="onclick_gamestate" android:id="@+id/header_gamestate" /> @@ -54,10 +53,11 @@ android:rotation="180" /> + android:layout_gravity="center_vertical" + android:visibility="invisible" /> @@ -117,29 +117,13 @@ - - - - + android:layout_height="match_parent">