]> hydra-www.ietfng.org Git - acmetensortoys-ctfws-android/commitdiff
Improve notification
authorNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Mon, 20 Feb 2017 21:30:12 +0000 (16:30 -0500)
committerNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Mon, 20 Feb 2017 21:30:12 +0000 (16:30 -0500)
mobile/build.gradle
mobile/src/main/AndroidManifest.xml
mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplayLocal.java
mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java
mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainService.java
mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainServiceNotification.java [new file with mode: 0644]
mobile/src/main/res/values/strings.xml
mobile/src/main/res/xml/preferences.xml

index 417f978e2557c02fed96c7cc80dbb443f1434678..34d48884e5a48dfebb6eb9837ec9f88e4ad75a5d 100644 (file)
@@ -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"
     }
index f61d74d61f765cb302f3b95586f6b50b04843ddf..19754ef6620b0ce5cd6e1c3bb950f6f7ca27844c 100644 (file)
@@ -32,7 +32,7 @@
         <service
             android:name=".MainService"
             android:enabled="true"
-            android:exported="false"></service>
+            android:exported="false" />
     </application>
 
 </manifest>
\ No newline at end of file
index 975af5c91e200b5e1f65a67445029a89b49b20d4..e7805330d9af1595d97da7e939b1147ea2328f64 100644 (file)
@@ -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;
index 78a1e1f98cf64d1720ec7f1a523362609ee5ceb9..000a98daa155a79f09a5447f01c683d0bffddd6b 100644 (file)
@@ -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);
index 507d8401c551086ad1b7695c72ea0885100dbecf..399e1b127d1bd103974579956fd554eb4a81f017 100644 (file)
@@ -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<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 {
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 (file)
index 0000000..21da30c
--- /dev/null
@@ -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<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;
+            }
+        }
+    }
+
+}
index eac3e141491cf85d7b5bba41d70b6ac9d95172b3..a5360ab62781057b1b1c479aeed6bba9f1238fb1 100644 (file)
@@ -6,17 +6,20 @@
     <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>
@@ -26,6 +29,9 @@
     <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">&lt;&lt;null&gt;&gt;</string>
 
     <string name="about_text"><![CDATA[
index d8e14ae7ff6a0894b6070a676bfe0f8093002ee4..a3734ac0a48b7c554a6b1f52afcd67ba4f03bacf 100644 (file)
@@ -6,9 +6,9 @@
         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