From bc390236cd50b3e886b602c7bcaa2ca2a7cd6a8c Mon Sep 17 00:00:00 2001 From: Nathaniel Wesley Filardo Date: Mon, 20 Feb 2017 16:30:12 -0500 Subject: [PATCH] Improve notification --- mobile/build.gradle | 2 +- mobile/src/main/AndroidManifest.xml | 2 +- .../ctfwstimer/CtFwSDisplayLocal.java | 1 - .../ctfwstimer/MainActivity.java | 2 +- .../ctfwstimer/MainService.java | 105 +----------- .../ctfwstimer/MainServiceNotification.java | 149 ++++++++++++++++++ mobile/src/main/res/values/strings.xml | 14 +- mobile/src/main/res/xml/preferences.xml | 4 +- 8 files changed, 170 insertions(+), 109 deletions(-) create mode 100644 mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java diff --git a/mobile/build.gradle b/mobile/build.gradle index 417f978..34d4888 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -7,7 +7,7 @@ android { applicationId "com.acmetensortoys.ctfwstimer" minSdkVersion 19 targetSdkVersion 25 - versionCode 3 + versionCode 4 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index f61d74d..19754ef 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -32,7 +32,7 @@ + android:exported="false" /> \ 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 975af5c..e780533 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java @@ -246,7 +246,6 @@ class CtFwSDisplayLocal implements CtFwSGameState.Observer { } }); } else { - CtFwSGameState.Msg m = msgs.get(s - 1); long td = (m.when == 0) ? 0 : (gs.isConfigured()) ? m.when - gs.getStartT() : 0; diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java index 78a1e1f..000a98d 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java @@ -105,7 +105,7 @@ public class MainActivity extends AppCompatActivity { 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); diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java index 507d840..399e1b1 100644 --- a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java @@ -1,19 +1,13 @@ 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; @@ -36,8 +30,8 @@ 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 + static final int NOTE_ID_USER = 1; + private Handler mHandler; // set in onCreate // The reason we're here! private final CtFwSGameState mCgs @@ -59,9 +53,9 @@ public class MainService extends Service { }); private CtFwSCallbacksMQTT mCtfwscbs = new CtFwSCallbacksMQTT(mCgs); - public MainService() { - mCgs.registerObserver(mCgsObserver); - } + private MainServiceNotification mMsn; // set in onCreate + + public MainService() { } // MQTT client management @@ -262,99 +256,12 @@ public class MainService extends Service { } } - // 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 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 { diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java new file mode 100644 index 0000000..21da30c --- /dev/null +++ b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java @@ -0,0 +1,149 @@ +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 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; + } + } + } + +} diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index eac3e14..a5360ab 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -6,17 +6,20 @@ Jailbreak\n%1$d of %2$d %1$d Flags: + Default + Game State: Game %1$d State: Game\nTime\nElapsed Messages: - OK - Cancel - Default + The game\'s afoot! + Flag captured! Red:%1$d Yellow:%2$d + Game ending soon + Setup phase + Jailbreak %1$d of %2$d About - Set MQTT Server Settings Connection Metadata: @@ -26,6 +29,9 @@ Disconnected Subscribed + Set MQTT Server + Vibrate? + <<null>> + android:title="@string/preftext_mqtt" /> \ No newline at end of file -- 2.50.1