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
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 {
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 {
}
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) {
pb_jb.setProgress((int) (now.roundEnd - System.currentTimeMillis() / 1000));
}
});
+ ch_jb.setVisibility(View.VISIBLE);
ch_jb.start();
}
});
ch.setOnChronometerTickListener(null);
ch.setBase(SystemClock.elapsedRealtime());
ch.stop();
+ ch.setVisibility(View.INVISIBLE);
}
});
}
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;
}
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);
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;
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();
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();
// 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?
@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;
mHandler.removeCallbacks(r);
}
});
- private final CtFwSCallbacksMQTT mCtfwscbs = new CtFwSCallbacksMQTT(mCgs);
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private MainServiceNotification mMsn; // set in onCreate
@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);
@Override
public void connectionLost(Throwable cause) {
Log.d("CtFwS", "Conn Lost: " + cause, cause);
+ mCtfwscbs.dispose();
+ mCtfwscbs = null;
setMSE(MqttServerEvent.MSE_DISCONN);
}
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) {
} 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
}
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
// *&@#&^*#@#&@#&@#
}
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. :)
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) {
// 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 {
}
}
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) {
}
}
void unregisterObserver(Observer o) {
- synchronized(this) { mObsvs.remove(o); }
+ synchronized(MainService.this) { mObsvs.remove(o); }
}
}
private final LocalBinder mBinder = new LocalBinder();
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;
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;
userNoteBuilder.setSubText(now.rationale);
}
- vibrate(VibrationSource.BREAK);
+ notifyUserSomehow(NotificationSource.BREAK);
ensureNotification();
} else {
// game no longer afoot
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),
// 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();
}
// 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:
// 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;
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" />
android:rotation="180" />
<Chronometer
+ android:id="@+id/ch_jailbreak"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:id="@+id/ch_jailbreak"
- android:layout_gravity="center_vertical" />
+ android:layout_gravity="center_vertical"
+ android:visibility="invisible" />
</TableRow>
</TableLayout>
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:text="@string/header_messages" />
-
- <TextView
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:inputType="none"
- android:ems="10"
- android:id="@+id/msgs"
- android:lines="10"
- android:scrollbars="vertical"
- android:gravity="bottom" />
-
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableRow
android:layout_width="match_parent"
- android:layout_height="match_parent" >
+ android:layout_height="match_parent">
<Button
android:id="@+id/btn_wait_short"
android:layout_weight="1"
android:indeterminate="false"
android:padding="5dp"
- android:visibility="invisible" />
+ android:visibility="invisible"
+ android:rotation="180"/>
<Chronometer
android:id="@+id/ch_wait_short"
<TableRow
android:layout_width="match_parent"
- android:layout_height="match_parent" >
+ android:layout_height="match_parent">
<Button
android:id="@+id/btn_wait_long"
android:layout_weight="1"
android:indeterminate="false"
android:padding="5dp"
- android:visibility="invisible" />
+ android:visibility="invisible"
+ android:rotation="180"/>
<Chronometer
android:id="@+id/ch_wait_long"
</TableRow>
</TableLayout>
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:text="@string/header_messages" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="none"
+ android:ems="10"
+ android:id="@+id/msgs"
+ android:lines="10"
+ android:scrollbars="vertical"
+ android:gravity="bottom" />
+
</LinearLayout>
<LinearLayout
<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:android="http://schemas.android.com/apk/res/android">
<item
- android:visible="true"
+ android:id="@+id/menu_reconn"
+ android:checkable="false"
android:enabled="true"
- android:title="@string/menutext_prf"
+ android:icon="@android:drawable/ic_menu_search"
+ android:title="@string/menutext_reconn"
+ android:visible="true"
+ app:showAsAction="ifRoom" />
+ <item
android:id="@+id/menu_prf"
+ android:checkable="false"
+ android:enabled="true"
android:icon="@android:drawable/ic_menu_manage"
- android:checkable="false"/>
- <item android:title="@string/menutext_about"
- android:id="@+id/menu_about"
- android:icon="@android:drawable/ic_menu_compass"
+ android:title="@string/menutext_prf"
android:visible="true"
+ app:showAsAction="never" />
+ <item
+ android:id="@+id/menu_about"
+ android:checkable="false"
android:enabled="true"
- android:checkable="false" />
+ android:icon="@android:drawable/ic_menu_help"
+ android:title="@string/menutext_about"
+ android:visible="true" />
</menu>
\ No newline at end of file
<string name="preftext_vibrate_jb">Vibrate on Jailbreak?</string>
<string name="preftext_vibrate_flag">Vibrate on Flag Capture?</string>
<string name="preftext_vibrate_mesg">Vibrate on Message?</string>
+ <string name="preftext_sound_jb">Sound on Jailbreak?</string>
+ <string name="preftext_sound_flag">Sound on Flag Capture?</string>
+ <string name="preftext_sound_mesg">Sound on Message?</string>
<string name="string_null"><<null>></string>
- <string name="wait_long">Wait 60</string>
- <string name="wait_short">Wait 10</string>
+ <string name="wait_long">Stun 60</string>
+ <string name="wait_short">Stun 10</string>
<string name="about_imagealt">The CMUKGB Shield Logo</string>
<string name="about_text"><![CDATA[
</center>
]]>
</string>
+ <string name="menutext_reconn">Reconnect</string>
</resources>
android:key="server"\r
android:defaultValue="@string/server_default"\r
android:title="@string/preftext_mqtt" />\r
+\r
<CheckBoxPreference\r
android:defaultValue="true"\r
android:key="prf_vibr_jb"\r
android:title="@string/preftext_vibrate_jb" />\r
+ <CheckBoxPreference\r
+ android:defaultValue="true"\r
+ android:key="prf_sound_jb"\r
+ android:title="@string/preftext_sound_jb" />\r
+\r
<CheckBoxPreference\r
android:layout_width="wrap_content"\r
android:layout_height="wrap_content"\r
android:defaultValue="true"\r
android:key="prf_vibr_flag"\r
android:title="@string/preftext_vibrate_flag" />\r
+ <CheckBoxPreference\r
+ android:layout_width="wrap_content"\r
+ android:layout_height="wrap_content"\r
+ android:defaultValue="true"\r
+ android:key="prf_sound_flag"\r
+ android:title="@string/preftext_sound_flag" />\r
+\r
<CheckBoxPreference\r
android:layout_width="wrap_content"\r
android:layout_height="wrap_content"\r
android:defaultValue="true"\r
android:key="prf_vibr_mesg"\r
android:title="@string/preftext_vibrate_mesg" />\r
+ <CheckBoxPreference\r
+ android:layout_width="wrap_content"\r
+ android:layout_height="wrap_content"\r
+ android:defaultValue="true"\r
+ android:key="prf_vibr_mesg"\r
+ android:title="@string/preftext_sound_mesg" />\r
+\r
</PreferenceScreen>\r
android {
compileSdkVersion 25
- buildToolsVersion "25.0.3"
+ buildToolsVersion '26.0.2'
defaultConfig {
applicationId "com.acmetensortoys.ctfwstimer"
minSdkVersion 21