]> hydra-www.ietfng.org Git - acmetensortoys-ctfws-android/commitdiff
Initial revision
authorNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Thu, 2 Feb 2017 23:42:05 +0000 (18:42 -0500)
committerNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Thu, 2 Feb 2017 23:42:05 +0000 (18:42 -0500)
Logic seems to work, UI is super terrible

32 files changed:
.gitignore [new file with mode: 0644]
build.gradle [new file with mode: 0644]
gradle.properties [new file with mode: 0644]
mobile/.gitignore [new file with mode: 0644]
mobile/build.gradle [new file with mode: 0644]
mobile/proguard-rules.pro [new file with mode: 0644]
mobile/src/main/AndroidManifest.xml [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplay.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSGameState.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java [new file with mode: 0644]
mobile/src/main/res/layout/activity_main.xml [new file with mode: 0644]
mobile/src/main/res/mipmap-hdpi/ic_launcher.png [new file with mode: 0644]
mobile/src/main/res/mipmap-mdpi/ic_launcher.png [new file with mode: 0644]
mobile/src/main/res/mipmap-xhdpi/ic_launcher.png [new file with mode: 0644]
mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png [new file with mode: 0644]
mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png [new file with mode: 0644]
mobile/src/main/res/values-w820dp/dimens.xml [new file with mode: 0644]
mobile/src/main/res/values/colors.xml [new file with mode: 0644]
mobile/src/main/res/values/dimens.xml [new file with mode: 0644]
mobile/src/main/res/values/strings.xml [new file with mode: 0644]
mobile/src/main/res/values/styles.xml [new file with mode: 0644]
settings.gradle [new file with mode: 0644]
wear/.gitignore [new file with mode: 0644]
wear/build.gradle [new file with mode: 0644]
wear/proguard-rules.pro [new file with mode: 0644]
wear/src/main/AndroidManifest.xml [new file with mode: 0644]
wear/src/main/res/mipmap-hdpi/ic_launcher.png [new file with mode: 0644]
wear/src/main/res/mipmap-mdpi/ic_launcher.png [new file with mode: 0644]
wear/src/main/res/mipmap-xhdpi/ic_launcher.png [new file with mode: 0644]
wear/src/main/res/mipmap-xxhdpi/ic_launcher.png [new file with mode: 0644]
wear/src/main/res/values/strings.xml [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..39fb081
--- /dev/null
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/build.gradle b/build.gradle
new file mode 100644 (file)
index 0000000..74b2ab0
--- /dev/null
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:2.2.3'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644 (file)
index 0000000..aac7c9b
--- /dev/null
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/mobile/.gitignore b/mobile/.gitignore
new file mode 100644 (file)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/mobile/build.gradle b/mobile/build.gradle
new file mode 100644 (file)
index 0000000..31f6c5a
--- /dev/null
@@ -0,0 +1,46 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 25
+    buildToolsVersion "24.0.3"
+    defaultConfig {
+        applicationId "com.acmetensortoys.ctfwstimer"
+        minSdkVersion 19
+        targetSdkVersion 24
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+        debug {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+repositories {
+    maven {
+        url "https://repo.eclipse.org/content/repositories/paho-releases/"
+    }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+        exclude group: 'com.android.support', module: 'support-annotations'
+    })
+    wearApp project(':wear')
+
+    compile('org.eclipse.paho:org.eclipse.paho.android.service:1.1.1') {
+        exclude module: 'support-v4'
+    }
+    compile 'com.android.support:appcompat-v7:25.1.1'
+    compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
+    compile 'com.android.support:support-v4:25.1.1'
+    testCompile 'junit:junit:4.12'
+}
diff --git a/mobile/proguard-rules.pro b/mobile/proguard-rules.pro
new file mode 100644 (file)
index 0000000..4e7b00d
--- /dev/null
@@ -0,0 +1,22 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/nwf/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Work around some Paho packaging warnings that manifest as errors.
+# See http://github.com/eclipse/paho.mqtt.android/issues/79
+-keepattributes InnerClasses
+-dontoptimize
\ No newline at end of file
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..ba66053
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.acmetensortoys.ctfwstimer">
+
+    <!-- Required for Paho MQTT client -->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+
+    <!-- Seems like a reasonable idea? -->
+    <!--
+    <uses-permission android:name="android.permission.VIBRATE" />
+    -->
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name=".MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <service android:name="org.eclipse.paho.android.service.MqttService" />
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSCallbacksMQTT.java
new file mode 100644 (file)
index 0000000..c0b5540
--- /dev/null
@@ -0,0 +1,99 @@
+package com.acmetensortoys.ctfwstimer;
+
+import android.util.Log;
+
+import org.eclipse.paho.client.mqttv3.IMqttMessageListener;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+import java.util.NoSuchElementException;
+import java.util.Scanner;
+
+class CtFwSCallbacksMQTT {
+    final private CtFwSDisplay mCdl;
+    final private CtFwSGameState mCgs;
+
+    CtFwSCallbacksMQTT(CtFwSDisplay cdl, CtFwSGameState cgs) {
+        mCdl = cdl;
+        mCgs = cgs;
+    }
+
+    IMqttMessageListener onConfig = new IMqttMessageListener() {
+        @Override
+        public void messageArrived(String topic, MqttMessage message) throws Exception {
+            String tm = message.toString().trim();
+            Log.d("CtFwS", "Message(Config): " + tm);
+
+            switch (tm) {
+                case "none":
+                    mCgs.configured = false;
+                    break;
+                default:
+                    try {
+                        Scanner s = new Scanner(tm);
+                        mCgs.startT    =  s.nextLong();
+                        mCgs.setupD     = s.nextInt();
+                        mCgs.rounds     = s.nextInt();
+                        mCgs.roundD     = s.nextInt();
+                        mCgs.flagsTotal = s.nextInt();
+                        mCgs.configured = true;
+                    } catch (NoSuchElementException e) {
+                        mCgs.configured = false;
+                    }
+                    break;
+            }
+            mCdl.notifyGameState();
+        }
+    };
+
+    IMqttMessageListener onEnd = new IMqttMessageListener() {
+        @Override
+        public void messageArrived(String topic, MqttMessage message) throws Exception {
+            Log.d("CtFwS", "Message(End): " + message);
+            try {
+                mCgs.endT = Long.parseLong(message.toString());
+            } catch (NumberFormatException e) {
+                mCgs.endT = 0;
+            }
+            mCdl.notifyGameState();
+        }
+    };
+
+    IMqttMessageListener onFlags = new IMqttMessageListener() {
+        @Override
+        public void messageArrived(String topic, MqttMessage message) throws Exception {
+            String tm = message.toString().trim();
+            Log.d("CtFwS", "Message(Flags): " + tm);
+
+            switch(tm) {
+                case "?":
+                    mCgs.setFlags(false);
+                    break;
+                default:
+                    Scanner s = new Scanner(tm);
+                    try {
+                        mCgs.setFlags(true);
+                        mCgs.setFlags(s.nextInt(),s.nextInt());
+                    } catch (NumberFormatException e) {
+                        mCgs.setFlags(false);
+                    }
+            }
+            mCdl.notifyFlags();
+        }
+    };
+
+    IMqttMessageListener onMessage = new IMqttMessageListener() {
+        @Override
+        public void messageArrived(String topic, MqttMessage message) throws Exception {
+            Log.d("CtFwS", "Message(Broadcast): " + message);
+            mCdl.notifyMessage(message.toString());
+        }
+    };
+
+    IMqttMessageListener onPlayerMessage = new IMqttMessageListener() {
+        @Override
+        public void messageArrived(String topic, MqttMessage message) throws Exception {
+            Log.d("CtFwS", "Message(Players): " + message);
+            mCdl.notifyMessage(message.toString());
+        }
+    };
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplay.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSDisplay.java
new file mode 100644 (file)
index 0000000..4d3b3ca
--- /dev/null
@@ -0,0 +1,268 @@
+package com.acmetensortoys.ctfwstimer;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.widget.Chronometer;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+// TODO nwf is bad at UI design; someone who isn't him should improve this
+
+class CtFwSDisplay {
+    final private Activity mAct;
+    final private Handler mHandler;
+    final private CtFwSGameState mCgs;
+
+    private long lastMsgTimeMS = 0;
+
+    CtFwSDisplay(Activity a, Handler h, CtFwSGameState cgs) {
+        mAct = a;
+        mHandler = h;
+        mCgs = cgs;
+    }
+
+    final private Runnable mProber = new Runnable() {
+        @Override
+        public void run() {
+            notifyGameState();
+        }
+    };
+
+    void notifyGameState() {
+        final long nowMS = System.currentTimeMillis();
+        long nowET = SystemClock.elapsedRealtime(); // Chronometer timebase
+        final long tbcf = nowMS - nowET; // time base correction factor ("when we booted"-ish)
+
+        final CtFwSGameState.Now now = mCgs.getNow(nowMS / 1000);
+
+        Log.d("CtFwS", "Display game state; nowMS=" + nowMS + " r=" + now.round + " rs=" + now.roundStart + " re=" + now.roundEnd);
+
+        if (now.rationale != null) {
+            Log.d("CtFwS", "Rationale: " + now.rationale + " stop=" + now.stop);
+
+            // TODO: display rationale somewhere, probably by hiding the game state!
+
+            doReset();
+
+            mHandler.removeCallbacks(mProber);
+            if (!now.stop) {
+                mHandler.postDelayed(mProber, mCgs.startT*1000 - nowMS);
+            }
+
+            return;
+        }
+
+        // Otherwise, it's game on!
+
+        // Clear the mesage log if it looks like it's a new game in play
+        if (lastMsgTimeMS < mCgs.startT * 1000) {
+            final TextView msgs = (TextView) (mAct.findViewById(R.id.msgs));
+            msgs.post(new Runnable() {
+                @Override
+                public void run() {
+                    msgs.setText("");
+                }
+            });
+        }
+
+        mHandler.removeCallbacks(mProber);
+        mHandler.postDelayed(mProber, now.roundEnd * 1000 - nowMS);
+
+        {
+            final TextView tv_jb = (TextView) (mAct.findViewById(R.id.tv_jailbreak));
+            tv_jb.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (now.round == 0) {
+                        tv_jb.setText(R.string.ctfws_gamestart);
+                    } else if (now.round == mCgs.rounds) {
+                        tv_jb.setText(R.string.ctfws_gameend);
+                    } else {
+                        tv_jb.setText(
+                                String.format(mAct.getResources().getString(R.string.ctfws_jailbreak),
+                                now.round));
+                    }
+                }
+            });
+
+            final ProgressBar pb_jb = (ProgressBar) (mAct.findViewById(R.id.pb_jailbreak));
+            pb_jb.post(new Runnable() {
+                @Override
+                public void run() {
+                    pb_jb.setIndeterminate(false);
+                    if (now.round == 0) {
+                        pb_jb.setMax(mCgs.setupD - 1);
+                    } else {
+                        pb_jb.setMax(mCgs.roundD - 1);
+                    }
+                    pb_jb.setProgress(0);
+                }
+            });
+
+            final Chronometer ch_jb = (Chronometer) (mAct.findViewById(R.id.ch_jailbreak));
+            ch_jb.post(new Runnable() {
+                @Override
+                public void run() {
+                    ch_jb.setBase(now.roundEnd * 1000 - tbcf);
+                    ch_jb.setOnChronometerTickListener(new Chronometer.OnChronometerTickListener() {
+                        @Override
+                        public void onChronometerTick(Chronometer c) {
+                            pb_jb.setProgress((int)(now.roundEnd - System.currentTimeMillis()/1000) - 1);
+                        }
+                    });
+                    ch_jb.start();
+                }
+            });
+        }
+        if (now.round > 0){
+            final ProgressBar pb_gp = (ProgressBar) (mAct.findViewById(R.id.pb_gameProgress));
+            pb_gp.post(new Runnable() {
+                @Override
+                public void run() {
+                    pb_gp.setIndeterminate(false);
+                    pb_gp.setMax(mCgs.rounds * mCgs.roundD - 1);
+                    pb_gp.setProgress(0);
+                }
+            });
+
+            final Chronometer ch_gp = (Chronometer) (mAct.findViewById(R.id.ch_gameProgress));
+            ch_gp.post(new Runnable() {
+                @Override
+                public void run() {
+                    ch_gp.setBase((mCgs.startT + mCgs.setupD) * 1000 - tbcf);
+                    ch_gp.setOnChronometerTickListener(new Chronometer.OnChronometerTickListener() {
+                        @Override
+                        public void onChronometerTick(Chronometer c) {
+                            pb_gp.setProgress((int)(System.currentTimeMillis()/1000
+                                    - mCgs.startT - mCgs.setupD));
+                        }
+                    });
+                    ch_gp.start();
+                }
+            });
+        } else {
+            final ProgressBar pb_gp = (ProgressBar) (mAct.findViewById(R.id.pb_gameProgress));
+            pb_gp.post(new Runnable() {
+                @Override
+                public void run() {
+                    pb_gp.setIndeterminate(false);
+                    pb_gp.setMax(mCgs.rounds * mCgs.roundD - 1);
+                    pb_gp.setProgress(0);
+                }
+            });
+
+            final Chronometer ch_gp = (Chronometer) (mAct.findViewById(R.id.ch_gameProgress));
+            ch_gp.post(new Runnable() {
+                @Override
+                public void run() {
+                    ch_gp.setBase(nowMS - tbcf);
+                    ch_gp.setOnChronometerTickListener(null);
+                    ch_gp.stop();
+                }
+            });
+        }
+        {
+            final TextView tv_flags = (TextView) (mAct.findViewById(R.id.tv_flags_label));
+            tv_flags.post(new Runnable() {
+                @Override
+                public void run() {
+                    tv_flags.setText(
+                            String.format(mAct.getResources().getString(R.string.ctfws_flags),
+                                    mCgs.flagsTotal));
+                }
+            });
+        }
+    }
+
+    private void doReset() {
+        Log.d("CtFwS", "Display Reset");
+
+        {
+            final Chronometer ch = (Chronometer) (mAct.findViewById(R.id.ch_jailbreak));
+            ch.post(new Runnable() {
+                @Override
+                public void run() {
+                    ch.setOnChronometerTickListener(null);
+                    ch.setBase(SystemClock.elapsedRealtime());
+                    ch.stop();
+                }
+            });
+        }
+        {
+            final Chronometer ch = (Chronometer) (mAct.findViewById(R.id.ch_gameProgress));
+            ch.post(new Runnable() {
+                @Override
+                public void run() {
+                    ch.setOnChronometerTickListener(null);
+                    ch.setBase(SystemClock.elapsedRealtime());
+                    ch.stop();
+                }
+            });
+        }
+        {
+            final ProgressBar pb = (ProgressBar) (mAct.findViewById(R.id.pb_jailbreak));
+            pb.post(new Runnable() {
+                @Override
+                public void run() {
+                    pb.setIndeterminate(true);
+                }
+            });
+        }
+        {
+            final ProgressBar pb = (ProgressBar) (mAct.findViewById(R.id.pb_gameProgress));
+            pb.post(new Runnable() {
+                @Override
+                public void run() {
+                    pb.setIndeterminate(true);
+                }
+            });
+        }
+    }
+
+    void notifyFlags() {
+        // TODO: This stinks
+
+        final StringBuffer sb = new StringBuffer();
+        if (mCgs.configured) {
+            if (mCgs.flagsVisible) {
+                sb.append("r=");
+                sb.append(mCgs.flagsRed);
+                sb.append(" y=");
+                sb.append(mCgs.flagsYel);
+            } else {
+                sb.append("r=? y=?");
+            }
+        }
+
+        final TextView msgs = (TextView)(mAct.findViewById(R.id.tv_flags));
+        msgs.post(new Runnable() {
+            @Override
+            public void run() {
+                msgs.setText(sb);
+            }
+        });
+    }
+
+    void notifyMessage(String m) {
+        lastMsgTimeMS = System.currentTimeMillis();
+
+        final StringBuffer sb = new StringBuffer();
+        long ts = lastMsgTimeMS/1000 - mCgs.startT;
+        if (!mCgs.configured || ts < 0) { ts = 0; }
+        sb.append(DateUtils.formatElapsedTime(ts));
+        sb.append(": ");
+        sb.append(m);
+        sb.append("\n");
+
+        final TextView msgs = (TextView)(mAct.findViewById(R.id.msgs));
+        msgs.post(new Runnable() {
+            @Override
+            public void run() {
+                msgs.append(sb);
+            }
+        });
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSGameState.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/CtFwSGameState.java
new file mode 100644 (file)
index 0000000..3c12b2e
--- /dev/null
@@ -0,0 +1,65 @@
+package com.acmetensortoys.ctfwstimer;
+
+import java.util.NoSuchElementException;
+import java.util.Scanner;
+
+class CtFwSGameState {
+    boolean configured;
+    long startT;     // NTP seconds for game start
+    int  setupD;
+    int  rounds;
+    int  roundD;
+    long endT = 0;   // NTP seconds for game end (if >= startT)
+
+    int  flagsTotal;
+    boolean flagsVisible = false;
+    int  flagsRed = 0;
+    int  flagsYel = 0;
+
+    void setFlags(boolean visible) {
+        flagsVisible = visible;
+    }
+    void setFlags(int red, int yel) {
+        flagsRed = red; flagsYel = yel;
+    }
+
+    class Now {
+        String rationale = null; // null if game is in play, otherwise other fields invalid
+        int round = 0;  // 0 for setup
+        long roundStart = 0, roundEnd = 0; // NTP seconds
+        boolean stop = false;
+    }
+    public Now getNow(long now) {
+        Now res = new Now();
+        if (!configured) {
+            res.rationale = "Game not configured";
+            res.stop = true;
+        } else if (endT >= startT) {
+            res.rationale = "Game over!";
+            res.stop = true;
+        } else if (now <= startT) {
+            res.rationale = "Start time in the future!";
+        }
+        if (res.rationale != null) {
+            return res;
+        }
+        long elapsed = now - startT;
+        if (elapsed < setupD) {
+            res.round = 0;
+            res.roundStart = startT;
+            res.roundEnd = startT + setupD;
+            return res;
+        }
+        elapsed -= setupD;
+        res.round = (int)(elapsed / roundD);
+        if (res.round >= rounds) {
+            res.rationale = "Game over!";
+            res.stop = true;
+            return res;
+        }
+        res.roundStart = startT + setupD + (res.round * roundD);
+        res.roundEnd = res.roundStart + roundD;
+        res.round += 1;
+        return res;
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java b/mobile/src/main/java/com/acmetensortoys/ctfwstimer/MainActivity.java
new file mode 100644 (file)
index 0000000..a9d3ea2
--- /dev/null
@@ -0,0 +1,149 @@
+package com.acmetensortoys.ctfwstimer;
+
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.util.Log;
+
+import org.eclipse.paho.android.service.MqttAndroidClient;
+import org.eclipse.paho.client.mqttv3.IMqttActionListener;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.IMqttToken;
+import org.eclipse.paho.client.mqttv3.MqttCallbackExtended;
+import org.eclipse.paho.client.mqttv3.MqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+
+public class MainActivity extends AppCompatActivity {
+
+    static private final String mqttClientId = MqttClient.generateClientId();
+    private MqttAndroidClient mMqc;
+
+    private final CtFwSGameState mCgs = new CtFwSGameState();
+    private CtFwSDisplay mCdl; // set in onCreate
+    private CtFwSCallbacksMQTT mCtfwscbs ; // set in onCreate
+
+    private synchronized void doMqtt(@Nullable String server) {
+        // Hang up on an existing connection, if we have one
+        synchronized (this) {
+            if (mMqc != null) {
+                mMqc.close();
+            }
+            mMqc = null;
+            mCgs.configured = false;
+            mCdl.notifyGameState();
+        }
+
+        // If that's all we were told to do, we're done
+        if (server == null) { return ; }
+
+        // We'll use this common callback object for our subscriptions below
+        final IMqttActionListener subal = new IMqttActionListener() {
+            @Override
+            public void onSuccess(IMqttToken asyncActionToken) {
+                Log.d("CtFwS", "Sub OK: " + asyncActionToken);
+            }
+
+            @Override
+            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                Log.e("CtFws", "Sub Fail: " + asyncActionToken, exception);
+            }
+        };
+
+        // Make our MQTT client and grab callbacks on *everything in sight*
+        final MqttAndroidClient mqc = new MqttAndroidClient(this,server, mqttClientId);
+        mqc.setCallback(new MqttCallbackExtended() {
+            @Override
+            public void connectComplete(boolean reconnect, String serverURI) {
+                Log.d("CtFwS", "Conn OK 2 srv=" + serverURI + " reconn=" + reconnect);
+                try {
+                    String p = "ctfws/game/";
+                    mqc.subscribe(p+"config"        , 2, null, subal, mCtfwscbs.onConfig);
+                    mqc.subscribe(p+"endtime"       , 2, null, subal, mCtfwscbs.onEnd);
+                    mqc.subscribe(p+"flags"         , 2, null, subal, mCtfwscbs.onFlags);
+                    mqc.subscribe(p+"message"       , 2, null, subal, mCtfwscbs.onMessage);
+                    mqc.subscribe(p+"message/player", 2, null, subal, mCtfwscbs.onPlayerMessage);
+                } catch (MqttException e) {
+                    Log.e("CtFwS", "Exn Sub", e);
+                }
+            }
+
+            @Override
+            public void connectionLost(Throwable cause) {
+                Log.d("CtFwS", "Conn Lost", cause);
+            }
+
+            @Override
+            public void messageArrived(String topic, MqttMessage message) throws Exception {
+                Log.d("CtFwS", "Message(Generic) " + topic + " : '" + message + "'" );
+            }
+
+            @Override
+            public void deliveryComplete(IMqttDeliveryToken token) {
+                // Unused, as we never publish
+                Log.d("CtFwS", "Delivery OK");
+            }
+        });
+
+
+        // Ahem.  Now then.  Connect with *more callbacks*, which will fire off our
+        // subscription requests, which of course have *yet more* callbacks, which
+        // react to messages sent to us.  Have we lost the thread yet?
+        try {
+            MqttConnectOptions mco = new MqttConnectOptions();
+            mco.setAutomaticReconnect(true);
+            mco.setKeepAliveInterval(180); // seconds
+
+            mqc.connect(mco, null, new IMqttActionListener() {
+                @Override
+                public void onSuccess(IMqttToken asyncActionToken) {
+                    Log.d("CtFwS", "Conn OK 1");
+                }
+
+                @Override
+                public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                    Log.e("CtFws", "Conn Fail", exception);
+                }
+            });
+
+        } catch (MqttException e) {
+            Log.e("CtFwS", "Conn Exn", e);
+        }
+
+        synchronized (this) {
+            if (BuildConfig.DEBUG && mMqc != null) { throw new AssertionError(); }
+            mMqc = mqc;
+        }
+    }
+
+    // Must hold strongly since Android only holds weakly once registered.
+    private SharedPreferences.OnSharedPreferenceChangeListener mOSPCL
+            = new SharedPreferences.OnSharedPreferenceChangeListener() {
+        @Override
+        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
+            switch(key) {
+                case "server":
+                    doMqtt(sharedPreferences.getString("server",null));
+                    break;
+            }
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        mCdl = new CtFwSDisplay(this, new Handler(), mCgs);
+        mCtfwscbs = new CtFwSCallbacksMQTT(mCdl, mCgs);
+
+        // TODO There really should be a UI thing for changing the server; we're all
+        // set for when-/if-ever that happens.
+
+        getPreferences(MODE_PRIVATE).registerOnSharedPreferenceChangeListener(mOSPCL);
+        doMqtt(getPreferences(MODE_PRIVATE).getString("server","tcp://nwf1.xen.prgmr.com:1883"));
+    }
+}
diff --git a/mobile/src/main/res/layout/activity_main.xml b/mobile/src/main/res/layout/activity_main.xml
new file mode 100644 (file)
index 0000000..381648e
--- /dev/null
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main"
+    android:layout_width="match_parent" android:layout_height="match_parent"
+    android:paddingBottom="@dimen/activity_vertical_margin"
+    android:paddingLeft="@dimen/activity_horizontal_margin"
+    android:paddingRight="@dimen/activity_horizontal_margin"
+    android:paddingTop="@dimen/activity_vertical_margin"
+    tools:context="com.acmetensortoys.ctfwstimer.MainActivity">
+
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <LinearLayout
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <TextView
+                android:text="@string/header_gamestate"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center" />
+
+            <TableLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:stretchColumns="1">
+
+                <TableRow
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:gravity="center"
+                    android:paddingBottom="1dp">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/tv_jailbreak"
+                        android:text="@string/ctfws_gamestart"
+                        android:gravity="center" />
+
+                    <ProgressBar
+                        style="@android:style/Widget.DeviceDefault.ProgressBar.Horizontal"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/pb_jailbreak"
+                        android:layout_weight="1"
+                        android:indeterminate="false"
+                        android:max="100"
+                        android:progress="0"
+                        android:padding="5dp"
+                        android:rotation="180" />
+
+                    <Chronometer
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/ch_jailbreak"
+                        android:countDown="true" />
+
+                </TableRow>
+
+                <TableRow
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:gravity="center"
+                    android:paddingBottom="1dp"
+                    android:paddingTop="1dp">
+
+                    <TextView
+                        android:text="@string/header_gametimeela"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/tv_gameProgress"
+                        android:layout_weight="1"
+                        android:gravity="center"
+                        android:layout_width="wrap_content" />
+
+                    <ProgressBar
+                        style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/pb_gameProgress"
+                        android:layout_weight="1"
+                        android:indeterminate="false"
+                        android:padding="5dp" />
+
+                    <Chronometer
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/ch_gameProgress" />
+
+                </TableRow>
+
+                <TableRow
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent"
+                    android:id="@+id/tr_flags"
+                    android:paddingTop="1dp">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:gravity="center"
+                        android:id="@+id/tv_flags_label" />
+
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/tv_flags"
+                        android:gravity="center" />
+
+                </TableRow>
+
+            </TableLayout>
+        </LinearLayout>
+
+        <LinearLayout
+            android:orientation="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <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:maxLines="10"
+                android:minLines="1"
+                android:scrollbars="vertical"
+                android:gravity="bottom" />
+        </LinearLayout>
+
+        <!--
+        <TableLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:id="@+id/tbl_opts"
+            android:stretchColumns="1">
+
+            <TableRow
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent" >
+
+                <TextView
+                    android:text="Notifications\nDuring\nGame"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:gravity="center" />
+
+                <LinearLayout
+                    android:orientation="horizontal"
+                    android:layout_width="match_parent"
+                    android:layout_height="match_parent">
+
+                    <CheckBox
+                        android:text="Wear"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/notif_wear"
+                        android:layout_weight="1" />
+
+                    <CheckBox
+                        android:text="Here"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:id="@+id/notif_here"
+                        android:layout_weight="1" />
+                </LinearLayout>
+
+            </TableRow>
+
+            <TableRow
+                android:layout_width="match_parent"
+                android:layout_height="match_parent" />
+
+        </TableLayout>
+        -->
+
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/mobile/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..cde69bc
Binary files /dev/null and b/mobile/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..c133a0c
Binary files /dev/null and b/mobile/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..bfa42f0
Binary files /dev/null and b/mobile/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..324e72c
Binary files /dev/null and b/mobile/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..aee44e1
Binary files /dev/null and b/mobile/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/mobile/src/main/res/values-w820dp/dimens.xml b/mobile/src/main/res/values-w820dp/dimens.xml
new file mode 100644 (file)
index 0000000..63fc816
--- /dev/null
@@ -0,0 +1,6 @@
+<resources>
+    <!-- Example customization of dimensions originally defined in res/values/dimens.xml
+         (such as screen margins) for screens with more than 820dp of available width. This
+         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
+    <dimen name="activity_horizontal_margin">64dp</dimen>
+</resources>
diff --git a/mobile/src/main/res/values/colors.xml b/mobile/src/main/res/values/colors.xml
new file mode 100644 (file)
index 0000000..3ab3e9c
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/mobile/src/main/res/values/dimens.xml b/mobile/src/main/res/values/dimens.xml
new file mode 100644 (file)
index 0000000..47c8224
--- /dev/null
@@ -0,0 +1,5 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+</resources>
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
new file mode 100644 (file)
index 0000000..c385cc9
--- /dev/null
@@ -0,0 +1,12 @@
+<resources>
+    <string name="app_name">CMUKGB CtFwS Timer</string>
+
+    <string name="ctfws_gameend">Game\nEnd</string>
+    <string name="ctfws_gamestart">Game\nStart</string>
+    <string name="ctfws_jailbreak">Jailbreak %1$d</string>
+    <string name="ctfws_flags">%1$d Flags:</string>
+
+    <string name="header_gamestate">Game State:</string>
+    <string name="header_gametimeela">Game\nTime\nElapsed</string>
+    <string name="header_messages">Messages:</string>
+</resources>
diff --git a/mobile/src/main/res/values/styles.xml b/mobile/src/main/res/values/styles.xml
new file mode 100644 (file)
index 0000000..5885930
--- /dev/null
@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>
diff --git a/settings.gradle b/settings.gradle
new file mode 100644 (file)
index 0000000..6a4e79f
--- /dev/null
@@ -0,0 +1 @@
+include ':mobile', ':wear'
diff --git a/wear/.gitignore b/wear/.gitignore
new file mode 100644 (file)
index 0000000..796b96d
--- /dev/null
@@ -0,0 +1 @@
+/build
diff --git a/wear/build.gradle b/wear/build.gradle
new file mode 100644 (file)
index 0000000..16de14e
--- /dev/null
@@ -0,0 +1,25 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 24
+    buildToolsVersion "24.0.3"
+    defaultConfig {
+        applicationId "com.acmetensortoys.ctfwstimer"
+        minSdkVersion 21
+        targetSdkVersion 24
+        versionCode 1
+        versionName "1.0"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+    compile 'com.google.android.support:wearable:2.0.0-alpha3'
+    compile 'com.google.android.gms:play-services-wearable:10.0.1'
+}
diff --git a/wear/proguard-rules.pro b/wear/proguard-rules.pro
new file mode 100644 (file)
index 0000000..23c6bbc
--- /dev/null
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/nwf/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
new file mode 100644 (file)
index 0000000..0dfd77f
--- /dev/null
@@ -0,0 +1,10 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.acmetensortoys.ctfwstimer">
+    <uses-feature android:name="android.hardware.type.watch" />
+    <application android:allowBackup="true" android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name" android:supportsRtl="true"
+        android:theme="@android:style/Theme.DeviceDefault">
+
+    </application>
+
+</manifest>
diff --git a/wear/src/main/res/mipmap-hdpi/ic_launcher.png b/wear/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..cde69bc
Binary files /dev/null and b/wear/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/wear/src/main/res/mipmap-mdpi/ic_launcher.png b/wear/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..c133a0c
Binary files /dev/null and b/wear/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/wear/src/main/res/mipmap-xhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..bfa42f0
Binary files /dev/null and b/wear/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png b/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644 (file)
index 0000000..324e72c
Binary files /dev/null and b/wear/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml
new file mode 100644 (file)
index 0000000..e54a99b
--- /dev/null
@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">CtFwS Timer</string>
+</resources>