applicationId "com.acmetensortoys.ctfwstimer"
minSdkVersion 19
targetSdkVersion 25
- versionCode 3
+ versionCode 4
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
<service
android:name=".MainService"
android:enabled="true"
- android:exported="false"></service>
+ android:exported="false" />
</application>
</manifest>
\ No newline at end of file
}
});
} else {
-
CtFwSGameState.Msg m = msgs.get(s - 1);
long td = (m.when == 0) ? 0 : (gs.isConfigured()) ? m.when - gs.getStartT() : 0;
if (mSrvBinder == null) {
Intent si = new Intent(this, MainService.class);
- bindService(si, ctfwssc, Context.BIND_AUTO_CREATE);
+ bindService(si, ctfwssc, Context.BIND_AUTO_CREATE | Context.BIND_ABOVE_CLIENT);
} else {
mSrvBinder.getGameState().registerObserver(mCdl);
mSrvBinder.registerObserver(mSrvObs);
package com.acmetensortoys.ctfwstimer;
-import android.app.PendingIntent;
import android.app.Service;
-import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
-import android.os.Vibrator;
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;
public class MainService extends Service {
// Android stuff
- private static final int NOTE_ID_USER = 1;
- private Handler mHandler; // set in OnCreate
+ static final int NOTE_ID_USER = 1;
+ private Handler mHandler; // set in onCreate
// The reason we're here!
private final CtFwSGameState mCgs
});
private CtFwSCallbacksMQTT mCtfwscbs = new CtFwSCallbacksMQTT(mCgs);
- public MainService() {
- mCgs.registerObserver(mCgsObserver);
- }
+ private MainServiceNotification mMsn; // set in onCreate
+
+ public MainService() { }
// MQTT client management
}
}
- // User-facing notification
- // TODO Move to its own display module?
-
- // The pattern for notification vibration patterns. Maybe we could have multiple for different
- // events, like flags/jailbreaks?
- private long[] VIBRATE_PATTERN = {0, 300, 200, 300};
-
- private void vibrate(long[] pattern) {
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
- // Cam: default value is "false" because we really don't want to be vibrating if we
- // accidentally lose our preferences somehow
- if (sp.getBoolean("prf_vibr", false)) {
- Vibrator v = (Vibrator) getSystemService(VIBRATOR_SERVICE);
- v.vibrate(VIBRATE_PATTERN,-1);
- }
- else {
- Log.d("vibrate", "off");
- }
- }
-
- private ServiceConnection userNoteSC;
- private void ensureNotification() {
- synchronized(this) {
- if (userNoteSC == null) {
- userNoteSC = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- }
- };
- }
- // Cam: Do we need this?
- bindService(new Intent(MainService.this, MainService.class), userNoteSC,
- Context.BIND_AUTO_CREATE);
- startForeground(NOTE_ID_USER, userNoteBuilder.build());
- }
- }
- private void ensureNoNotification() {
- synchronized (this) {
- if (userNoteSC != null) {
- stopForeground(true);
- unbindService(userNoteSC);
- userNoteSC = null;
- }
- }
- }
-
- private NotificationCompat.Builder userNoteBuilder;
- // TODO (Cam): It'd be cool if we could make the notification say "you got a flag" or something
- // for a few seconds after we get messages
- 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) {
- vibrate(VIBRATE_PATTERN);
- // game is afoot!
- userNoteBuilder.setContentTitle(
- now.rationale == null ? "Game is afoot!" : now.rationale);
- userNoteBuilder.setContentText(
- now.round == 0 ? "Setup phase" : ("Round " + now.round));
- ensureNotification();
- } else {
- // game no longer afoot
- ensureNoNotification();
- }
- }
-
- @Override
- public void onCtFwSFlags(CtFwSGameState game) { }
-
- // Cam: Are we just explicitly no-op'ing this, or should we actually display messages?
- @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));
+ mMsn = new MainServiceNotification(this, mCgs);
}
public class LocalBinder extends Binder {
--- /dev/null
+package com.acmetensortoys.ctfwstimer;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+
+import com.acmetensortoys.ctfwstimer.lib.CtFwSGameState;
+
+import java.util.List;
+
+class MainServiceNotification {
+ final private MainService mService;
+ private NotificationCompat.Builder userNoteBuilder;
+
+ MainServiceNotification(MainService ms, CtFwSGameState game){
+ mService = ms;
+
+ Intent ni = new Intent(ms, MainActivity.class);
+ ni.setAction(Intent.ACTION_MAIN);
+ ni.addCategory(Intent.CATEGORY_LAUNCHER);
+ ni.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ userNoteBuilder = new NotificationCompat.Builder(ms)
+ .setOnlyAlertOnce(false)
+ .setSmallIcon(R.drawable.shield1)
+ .setContentIntent(PendingIntent.getActivity(ms, 0, ni, 0));
+
+ game.registerObserver(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 or in the future!
+ Resources rs = mService.getResources();
+
+ if (now.rationale == null) {
+ userNoteBuilder.setSubText(rs.getString(R.string.notify_afoot));
+ if (now.round == 0) {
+ userNoteBuilder.setContentTitle(rs.getString(R.string.notify_gamestart));
+ } else if (now.round == game.getRounds()) {
+ userNoteBuilder.setContentTitle(rs.getString(R.string.notify_gameend));
+ } else {
+ userNoteBuilder.setContentTitle(
+ String.format(rs.getString(R.string.notify_jailbreak),
+ now.round, game.getRounds() - 1));
+ }
+ } else {
+ userNoteBuilder.setSubText(now.rationale);
+ }
+
+ vibrate(VIBRATE_PATTERN_NOW);
+ ensureNotification();
+ } else {
+ // game no longer afoot
+ ensureNoNotification();
+ }
+ }
+
+ @Override
+ public void onCtFwSFlags(CtFwSGameState game) {
+ // If flags are hidden or there aren't any captured (e.g. this is a notification
+ // of a reset to 0), don't do anything.
+ if (game.flagsVisible && (game.flagsRed + game.flagsYel > 0)) {
+ vibrate(VIBRATE_PATTERN_FLAG);
+ userNoteBuilder.setContentText(
+ String.format(mService.getResources().getString(R.string.notify_flags),
+ game.flagsRed, game.flagsYel));
+ refreshNotification();
+ }
+ }
+
+ @Override
+ public void onCtFwSMessage(CtFwSGameState game, List<CtFwSGameState.Msg> msgs) {
+ // Only do anything if we aren't clearing the message list
+ int s = msgs.size();
+ if (s != 0) {
+ vibrate(VIBRATE_PATTERN_MSG);
+ userNoteBuilder.setContentText(msgs.get(s - 1).msg);
+ refreshNotification();
+ }
+ }
+ });
+ }
+
+ private long[] VIBRATE_PATTERN_NOW = {0, 100, 100, 300, 100, 300, 100, 300}; // 'J' = .---
+ private long[] VIBRATE_PATTERN_FLAG = {0, 100, 100, 100, 100, 300, 100, 100}; // 'F' = ..-.
+ private long[] VIBRATE_PATTERN_MSG = {0, 300, 100, 300}; // 'M' = --
+
+ private void vibrate(long[] pattern) {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mService.getBaseContext());
+ // Cam: default value is "false" because we really don't want to be vibrating if we
+ // accidentally lose our preferences somehow
+ if (sp.getBoolean("prf_vibr", false)) {
+ userNoteBuilder.setVibrate(pattern);
+ }
+ else {
+ userNoteBuilder.setVibrate(null);
+ }
+ }
+
+ private ServiceConnection userNoteSC;
+ private void refreshNotification() {
+ synchronized (this) {
+ if (userNoteSC != null) {
+ mService.startForeground(MainService.NOTE_ID_USER, userNoteBuilder.build());
+ }
+ }
+ }
+ private void ensureNotification() {
+ synchronized(this) {
+ if (userNoteSC == null) {
+ userNoteSC = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ }
+ };
+ // Ensure that the service stays alive while the notification is active
+ mService.bindService(new Intent(mService, MainService.class), userNoteSC,
+ Context.BIND_AUTO_CREATE);
+ }
+ mService.startForeground(MainService.NOTE_ID_USER, userNoteBuilder.build());
+ }
+ }
+ private void ensureNoNotification() {
+ synchronized (this) {
+ if (userNoteSC != null) {
+ mService.stopForeground(true);
+ mService.unbindService(userNoteSC);
+ userNoteSC = null;
+ }
+ }
+ }
+
+}
<string name="ctfws_jailbreak">Jailbreak\n%1$d of %2$d</string>
<string name="ctfws_flags">%1$d Flags:</string>
+ <string name="dialog_reset">Default</string>
+
<string name="header_gamestate0">Game State:</string>
<string name="header_gamestateN">Game %1$d State:</string>
<string name="header_gametimeela">Game\nTime\nElapsed</string>
<string name="header_messages">Messages:</string>
- <string name="dialog_ok">OK</string>
- <string name="dialog_cancel">Cancel</string>
- <string name="dialog_reset">Default</string>
+ <string name="notify_afoot">The game\'s afoot!</string>
+ <string name="notify_flags">Flag captured! Red:%1$d Yellow:%2$d</string>
+ <string name="notify_gameend">Game ending soon</string>
+ <string name="notify_gamestart">Setup phase</string>
+ <string name="notify_jailbreak">Jailbreak %1$d of %2$d</string>
<string name="menutext_about">About</string>
- <string name="menutext_mqtt">Set MQTT Server</string>
<string name="menutext_prf">Settings</string>
<string name="mqtt_header">Connection Metadata:</string>
<string name="mqtt_disconn">Disconnected</string>
<string name="mqtt_subbed">Subscribed</string>
+ <string name="preftext_mqtt">Set MQTT Server</string>
+ <string name="preftext_vibrate">Vibrate?</string>
+
<string name="string_null"><<null>></string>
<string name="about_text"><![CDATA[
android:singleLine="true"\r
android:key="server"\r
android:defaultValue="tcp://ctfws-mqtt.ietfng.org:1883"\r
- android:title="@string/menutext_mqtt" />\r
+ android:title="@string/preftext_mqtt" />\r
<CheckBoxPreference\r
android:defaultValue="true"\r
- android:title="Vibrate?"\r
+ android:title="@string/preftext_vibrate"\r
android:key="prf_vibr" />\r
</PreferenceScreen>
\ No newline at end of file