]> hydra-www.ietfng.org Git - acmetensortoys-teled/commitdiff
Hack and slash and more refactoring
authorNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Sat, 20 Aug 2016 16:46:04 +0000 (12:46 -0400)
committerNathaniel Wesley Filardo <nwf@cs.jhu.edu>
Sat, 20 Aug 2016 16:46:04 +0000 (12:46 -0400)
Maybe making progress?

41 files changed:
.gitignore [new file with mode: 0644]
build.gradle
mobile/build.gradle
mobile/src/main/AndroidManifest.xml
mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/BehaviorFactories.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/BehaviorFactory.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/TeleDIOIOManager.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/IOIOBehaviors.java [deleted file]
mobile/src/main/java/com/acmetensortoys/android/teled/IOIOService.java [deleted file]
mobile/src/main/java/com/acmetensortoys/android/teled/MainActivity.java [deleted file]
mobile/src/main/java/com/acmetensortoys/android/teled/RemoteControlService.java [deleted file]
mobile/src/main/java/com/acmetensortoys/android/teled/SMSRecv.java [deleted file]
mobile/src/main/java/com/acmetensortoys/android/teled/Service/EphemeralTeleDService.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Service/PositionReporting.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Service/SMSRecv.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Service/TeleDService.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/UI/FragPrefs.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/UI/MainActivity.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/LocationUpdateBH.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/PendingIntentBH.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/RecurrentAlarmBH.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/BehaviorHandle.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/BehaviorHandleSet.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Subscribee.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/SubscribeeImpl.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/SuspendableBH.java [new file with mode: 0644]
mobile/src/main/java/com/acmetensortoys/android/teled/Utils/ThreadBehaviorHandle.java [new file with mode: 0644]
mobile/src/main/res/drawable/ic_info_black_24dp.xml [new file with mode: 0644]
mobile/src/main/res/drawable/ic_notifications_black_24dp.xml [new file with mode: 0644]
mobile/src/main/res/drawable/ic_sync_black_24dp.xml [new file with mode: 0644]
mobile/src/main/res/layout/app_bar_main.xml
mobile/src/main/res/layout/content_scrolling.xml
mobile/src/main/res/layout/fragment_main.xml [new file with mode: 0644]
mobile/src/main/res/layout/newmain.xml [new file with mode: 0644]
mobile/src/main/res/values-v21/styles.xml [deleted file]
mobile/src/main/res/values-w820dp/dimens.xml [deleted file]
mobile/src/main/res/values/strings.xml
mobile/src/main/res/values/styles.xml
mobile/src/main/res/xml/pref_frag_main.xml [new file with mode: 0644]
wear/build.gradle
wear/src/main/res/layout/activity_main.xml

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..c50ab7a
--- /dev/null
@@ -0,0 +1,6 @@
+*.iml
+.gradle
+/local.properties
+.DS_Store
+/build
+/captures
index e0b366a781a01939da379f5fe403919f948bf3ff..aff4f415e95080b78852dc945e3e1ac5e5687cb0 100644 (file)
@@ -5,7 +5,7 @@ buildscript {
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:1.5.0'
+        classpath 'com.android.tools.build:gradle:2.1.2'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
index 94efc614af0d48ee1b3975b91ce8fd85a00580e5..6a9f4ee21b7dda374198879b9ba9e0296d2fe489 100644 (file)
@@ -2,11 +2,11 @@ apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 23
-    buildToolsVersion "23.0.2"
+    buildToolsVersion "23.0.3"
 
     defaultConfig {
         applicationId "com.acmetensortoys.android.teled"
-        minSdkVersion 19
+        minSdkVersion 23
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
@@ -22,6 +22,14 @@ android {
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
     }
+
+    dataBinding { enabled = true }
+}
+
+dependencies {
+    compile fileTree(dir: 'libs', include: ['*.jar'])
+    wearApp project(':wear')
+    testCompile 'junit:junit:4.12'
 }
 
 // IOIO support
@@ -32,11 +40,23 @@ dependencies {
     compile 'com.github.ytai.ioio:IOIOLibAndroidDevice:5.07'
 }
 
+// Functional java lets us not invent our own names for things that any
+// respectable language ought to have had from the start. ;)
 dependencies {
-    compile fileTree(dir: 'libs', include: ['*.jar'])
-    wearApp project(':wear')
-    testCompile 'junit:junit:4.12'
-    compile 'com.android.support:appcompat-v7:23.1.1'
-    compile 'com.google.android.gms:play-services:+'
-    compile 'com.android.support:design:23.1.1'
+    compile 'org.functionaljava:functionaljava:4.5'
+    // compile 'org.functionaljava:functionaljava-java-core:4.5'
+}
+
+// Datastructures library
+dependencies {
+    compile group: 'com.google.guava', name: 'guava', version: '19.0'
 }
+
+// Additional Google-isms
+dependencies {
+    compile 'com.android.support:appcompat-v7:+'
+    compile 'com.android.support:design:+'
+    compile 'com.android.support:support-v4:23.+'
+    compile 'com.google.android.gms:play-services-location:9.0.2'
+}
+
index 8dbc75987b1ee809d2955645d21188170b5d55cb..9175defe4111952c3b4303f128f6cc48254860e2 100644 (file)
@@ -2,12 +2,18 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.acmetensortoys.android.teled">
 
-    <uses-permission android:name="android.permission.WAKE_LOCK" />
-    <uses-permission android:name="android.permission.SEND_SMS" />
-    <uses-permission android:name="android.permission.RECEIVE_SMS" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
-    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.BLUETOOTH" /> <!-- IOIO -->
 
+
+    <!-- <uses-permission android:name="android.permission.BODY_SENSORS" /> -->
+    <!-- <uses-permission android:name="android.permission.INTERNET" /> -->
+    <uses-permission android:name="android.permission.RECEIVE_SMS" /> <!-- SMSRecv -->
+    <uses-permission android:name="android.permission.SEND_SMS" /> <!--  -->
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+
+    <!-- <uses-permission android:name="android.permission.WAKE_LOCK" /> -->
     <application
         android:allowBackup="true"
         android:icon="@mipmap/ic_launcher"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
         <activity
-            android:name=".MainActivity"
-            android:label="@string/app_name"
-            android:theme="@style/AppTheme.NoActionBar">
+            android:name=".UI.MainActivity"
+            android:label="@string/app_name">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
+
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
 
         <service
-            android:name=".IOIOService"
-            android:enabled="true" />
-
-        <service
-            android:name=".RemoteControlService"
+            android:name=".Service.TeleDService"
             android:enabled="true" />
 
+        <!--
         <receiver
-            android:name=".SMSRecv"
+            android:name=".Service.SMSRecv"
             android:enabled="true"
             android:exported="true"
             android:permission="android.permission.RECEIVE_SMS">
                 <action android:name="android.provider.Telephony.SMS_RECEIVED" />
             </intent-filter>
         </receiver>
+        -->
+
+        <service
+            android:name=".Service.EphemeralTeleDService"
+            android:exported="false"></service>
     </application>
 
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/BehaviorFactories.java b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/BehaviorFactories.java
new file mode 100644 (file)
index 0000000..3fac1f8
--- /dev/null
@@ -0,0 +1,133 @@
+package com.acmetensortoys.android.teled.IOIO;
+
+import java.util.Collection;
+
+import fj.function.Effect1;
+import fj.Void;
+
+import ioio.lib.api.AnalogInput;
+import ioio.lib.api.Closeable;
+import ioio.lib.api.DigitalOutput;
+import ioio.lib.api.IOIO;
+import ioio.lib.api.PwmOutput;
+
+public class BehaviorFactories {
+    static private BehaviorFactory<Void> makeBlinkOnce(final int pinId, final int dur) {
+        return new BehaviorFactory<Void>() {
+            public Behavior<Void> create(IOIO ioio_, Collection<Closeable> cc)
+                    throws Exception {
+                final DigitalOutput pin = ioio_.openDigitalOutput(pinId);
+                cc.add(pin);
+                return new Behavior<Void>() {
+                    public void run() throws Exception {
+                        pin.write(false);
+                        Thread.sleep(dur);
+                        pin.write(true);
+                    }
+                    public void updateIn(Void v) { v.absurd(); }
+                };
+            }
+        };
+    }
+    static public BehaviorFactory<Void> makeBlinkLEDOnce(int dur) {
+        return makeBlinkOnce(IOIO.LED_PIN, dur);
+    }
+
+    static private BehaviorFactory<Void> makeBlinkMany(final int pinId, final int dur) {
+        return new BehaviorFactory<Void>() {
+            public Behavior<Void> create(IOIO ioio_, Collection<Closeable> cc)
+                    throws Exception {
+                final DigitalOutput pin = ioio_.openDigitalOutput(pinId);
+                cc.add(pin);
+                return new Behavior<Void>() {
+                    public void run() throws Exception {
+                        while(!Thread.currentThread().isInterrupted()) {
+                            pin.write(false);
+                            Thread.sleep(dur);
+                            pin.write(true);
+                            Thread.sleep(dur/2);
+                        }
+                    }
+                    public void updateIn(Void v) { v.absurd(); }
+                };
+            }
+        };
+    }
+    static public BehaviorFactory<Void> makeBlinkLEDMany(int dur) {
+        return makeBlinkMany(IOIO.LED_PIN, dur);
+    }
+
+    static public BehaviorFactory<Float> makePWMListener() {
+        return new BehaviorFactory<Float>() {
+            public Behavior<Float> create(IOIO ioio_, Collection<Closeable> cc) throws Exception {
+                final DigitalOutput motorEnable = ioio_.openDigitalOutput(1, false); // active low
+                cc.add(motorEnable);
+                final PwmOutput motorPwm = ioio_.openPwmOutput(2, 100);
+                cc.add(motorPwm);
+                return new Behavior<Float>() {
+                    private float v;
+
+                    public void run() throws Exception {
+                        while(!Thread.currentThread().isInterrupted()) {
+                            this.wait();
+                            motorPwm.setDutyCycle(v);
+                        }
+                    }
+
+                    public void updateIn(Float v) {
+                        this.v = v;
+                        this.notify();
+                    }
+                };
+            }
+        };
+    }
+
+    static public BehaviorFactory<Void> makeForceStreamer(final Effect1<Double> cb) {
+        return new BehaviorFactory<Void>() {
+            public Behavior<Void> create(IOIO ioio_, Collection<Closeable> cc) throws Exception {
+                final AnalogInput sensor = ioio_.openAnalogInput(31);
+                cc.add(sensor);
+                sensor.setBuffer(50);
+                return new Behavior<Void>() {
+                    public void run() throws Exception {
+                        while (!Thread.currentThread().isInterrupted()) {
+                            float v = sensor.getVoltageBuffered();
+                            cb.f((double)v);
+                        }
+                    }
+                    public void updateIn(Void v) { v.absurd(); }
+                };
+            }
+        };
+    }
+
+    static public BehaviorFactory<Void> makeForceFeedback(final Effect1<Double> cb) {
+        return new BehaviorFactory<Void>() {
+            public Behavior<Void> create(IOIO ioio_, Collection<Closeable> cc) throws Exception {
+                final AnalogInput sensor = ioio_.openAnalogInput(31);
+                cc.add(sensor);
+                sensor.setBuffer(50);
+                final DigitalOutput motorEnable = ioio_.openDigitalOutput(1, false); // active low
+                cc.add(motorEnable);
+                final PwmOutput motorPwm = ioio_.openPwmOutput(2, 100);
+                cc.add(motorPwm);
+                return new Behavior<Void>() {
+                    public void run() throws Exception {
+                        float setv = 0.0f;
+                        while (!Thread.currentThread().isInterrupted()) {
+                            float v = sensor.getVoltageBuffered();
+                            // Try to save some bandwidth by only updating if it matters.
+                            if (Math.abs(v - setv) > 0.1) {
+                                motorPwm.setDutyCycle(v / 3.3f);
+                                setv = v;
+                            }
+                            cb.f((double)v);
+                        }
+                    }
+                    public void updateIn(Void v) { v.absurd(); }
+                };
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/BehaviorFactory.java b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/BehaviorFactory.java
new file mode 100644 (file)
index 0000000..59957d3
--- /dev/null
@@ -0,0 +1,25 @@
+package com.acmetensortoys.android.teled.IOIO;
+
+import java.util.Collection;
+
+import ioio.lib.api.Closeable;
+import ioio.lib.api.IOIO;
+
+public interface BehaviorFactory<In> {
+    abstract class Behavior<In> {
+        @SuppressWarnings("EmptyMethod")
+        public void shutdown() { }
+        abstract public void updateIn(In i);
+        abstract public void run() throws Exception;
+    }
+
+    /*
+    The provided set of Closeable things is initially empty but can be
+    added to to simplify resource management inside the create() method and
+    in the returned Behavior: if create() raises an exception, the contents
+    of cc will be iterated and closed.  When it comes time to shut down the
+    behavior, again, cc will be iterated and closed.  Hooray mutation, I
+    guess; the cause of, and solution to, all of mutations' problems.
+    */
+    Behavior<In> create(IOIO i, Collection<Closeable> cc) throws Exception;
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/TeleDIOIOManager.java b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIO/TeleDIOIOManager.java
new file mode 100644 (file)
index 0000000..885b331
--- /dev/null
@@ -0,0 +1,121 @@
+// Based, in long past, on IOIOService example from Ytai's repository
+
+package com.acmetensortoys.android.teled.IOIO;
+
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.acmetensortoys.android.teled.Utils.BehaviorHandle;
+import com.acmetensortoys.android.teled.Utils.ThreadBehaviorHandle;
+import com.acmetensortoys.android.teled.Utils.Subscribee;
+import com.acmetensortoys.android.teled.Utils.SubscribeeImpl;
+
+import java.util.ArrayList;
+
+import fj.function.Effect1;
+
+import ioio.lib.api.Closeable;
+import ioio.lib.api.IOIO;
+import ioio.lib.api.exception.ConnectionLostException;
+import ioio.lib.util.BaseIOIOLooper;
+import ioio.lib.util.IOIOLooper;
+
+public class TeleDIOIOManager
+        implements ioio.lib.util.IOIOLooperProvider {
+    private IOIO ioio = null; // If we have one connected, here 'tis.
+
+    // TODO We should have some state that lets us identify which IOIO we mean
+    // which should be configurable through the UI.
+
+    // Subscribe or unsubscribe to the IOIO being connected;
+    // when we are ready, the behaviors offered below will work; when
+    // not, they won't.  On transition from ready to unready, all the
+    // threads will die of their own accord as their closed-over
+    // objects which reach into the IOIO issue exceptions.
+    private SubscribeeImpl<Boolean> ioioStatus = new SubscribeeImpl<>(false);
+    public Subscribee<Boolean> getStatus() { return ioioStatus; }
+
+    @Override
+    public IOIOLooper createIOIOLooper(final String type, Object info) {
+        if (type.equals("ioio.lib.android.bluetooth.BluetoothIOIOConnection")) {
+            Object[] info_ = (Object[]) info;
+            Log.d("TDIS", "Looper creating for " + info_[0].toString() + ":" + info_[1].toString());
+        }
+
+        // TODO Check to see if it is the right IOIO.
+
+        return new BaseIOIOLooper() {
+            @Override
+            protected void setup() throws ConnectionLostException,
+                    InterruptedException {
+                synchronized (TeleDIOIOManager.this) {
+                    if (ioio != null) {
+                        throw new RuntimeException("Aaaaaaa! Setup with existing IOIO?");
+                    }
+                    ioio = ioio_;
+                    ioioStatus.publish(true);
+                }
+            }
+
+            // All action takes place in the worker threads which are spawned below.
+            // So here we just park ourselves in a waiter and hope for the best.
+            @Override
+            public void loop() throws ConnectionLostException,
+                    InterruptedException {
+                ioio.waitForDisconnect();
+            }
+
+            @Override
+            public void disconnected() {
+                synchronized (TeleDIOIOManager.this) {
+                    ioioStatus.publish(false);
+                    TeleDIOIOManager.this.ioio = null;
+                }
+            }
+        };
+    }
+
+    @Nullable
+    public <In> BehaviorHandle<In> makeBehaviorThread(BehaviorFactory<In> bf) {
+        IOIO ioio_;
+        synchronized (TeleDIOIOManager.this) {
+            ioio_ = ioio;
+        }
+        if (ioio_ == null) {
+            return null;
+        }
+        final BehaviorFactory.Behavior<In> b;
+        final ArrayList<Closeable> cc = new ArrayList<>(5);
+        try {
+            b = bf.create(ioio_, cc);
+        } catch (Exception e) {
+            for (Closeable c : cc) { c.close(); }
+            return null;
+        }
+        final SubscribeeImpl<BehaviorHandle.BehaviorState> blni
+                = new SubscribeeImpl<>(BehaviorHandle.BehaviorState.NEW);
+        return new ThreadBehaviorHandle<>(new Thread() {
+            public void run() {
+                try {
+                    blni.publish(BehaviorHandle.BehaviorState.RUNNING);
+                    b.run();
+                } catch (Exception e) {
+                    // ignored; the thread is allowed to just die;
+                    // anyone curious can use .isAlive() or .join().
+                    // They're also welcome to .interrupt()
+                    Log.e("IOIOBehavior", "Exiting from exception", e);
+                } finally {
+                    b.shutdown();
+                    for (Closeable c : cc) { c.close(); }
+                    // Publish DEAD state after all resources have been released,
+                    // in case one of our subscribers wants to claim them.
+                    blni.publish(BehaviorHandle.BehaviorState.DEAD);
+                }
+            }
+        }, new Effect1<In>() {
+            public void f(In x) {
+                b.updateIn(x);
+            }
+        }, blni);
+    }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOBehaviors.java b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOBehaviors.java
deleted file mode 100644 (file)
index 2293ae3..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.acmetensortoys.android.teled;
-
-import java.util.Collection;
-
-import ioio.lib.api.AnalogInput;
-import ioio.lib.api.Closeable;
-import ioio.lib.api.DigitalOutput;
-import ioio.lib.api.IOIO;
-import ioio.lib.api.PwmOutput;
-
-public class IOIOBehaviors {
-    static abstract class Factory {
-        abstract class Behavior {
-            @SuppressWarnings("EmptyMethod")
-            void shutdown() { }
-            abstract void run() throws Exception;
-        }
-        /*
-        The provided set of Closeable things is initially empty but can be
-        added to to simplify resource management inside the create() method and
-        in the returned Behavior: if create() raises an exception, the contents
-        of cc will be iterated and closed.  When it comes time to shut down the
-        behavior, again, cc will be iterated and closed.  Hooray mutation, I
-        guess; the cause of, and solution to, all of mutations' problems.
-        */
-        abstract Behavior create(IOIO i, Collection<Closeable> cc) throws Exception;
-    }
-
-    static private Factory makeBlinkOnce(final int pinId, final int dur) {
-        return new Factory() {
-            public Behavior create(IOIO ioio_, Collection<Closeable> cc)
-                    throws Exception {
-                final DigitalOutput pin = ioio_.openDigitalOutput(pinId);
-                cc.add(pin);
-                return new Behavior() {
-                    public void run() throws Exception {
-                        pin.write(false);
-                        Thread.sleep(dur);
-                        pin.write(true);
-                    }
-                };
-            }
-        };
-    }
-    static public Factory makeBlinkLEDOnce(int dur) {
-        return makeBlinkOnce(IOIO.LED_PIN, dur);
-    }
-
-    static private Factory makeBlinkMany(final int pinId, final int dur) {
-        return new Factory() {
-            public Behavior create(IOIO ioio_, Collection<Closeable> cc)
-                    throws Exception {
-                final DigitalOutput pin = ioio_.openDigitalOutput(pinId);
-                cc.add(pin);
-                return new Behavior() {
-                    public void run() throws Exception {
-                        while(true) {
-                            pin.write(false);
-                            Thread.sleep(dur);
-                            pin.write(true);
-                            Thread.sleep(dur/2);
-                        }
-                    }
-                };
-            }
-        };
-    }
-    static public Factory makeBlinkLEDMany(int dur) {
-        return makeBlinkMany(IOIO.LED_PIN, dur);
-    }
-
-    public interface ForceFeedbackCallback {
-        void run(float v);
-    }
-    static public Factory makeForceFeedback(final ForceFeedbackCallback cb) {
-        return new Factory() {
-            public Behavior create(IOIO ioio_, Collection<Closeable> cc) throws Exception {
-                final AnalogInput sensor = ioio_.openAnalogInput(31);
-                cc.add(sensor);
-                sensor.setBuffer(50);
-                final DigitalOutput motorEnable = ioio_.openDigitalOutput(1, false); // active low
-                cc.add(motorEnable);
-                final PwmOutput motorPwm = ioio_.openPwmOutput(2, 100);
-                cc.add(motorPwm);
-                return new Behavior() {
-                    public void run() throws Exception {
-                        float setv = 0.0f;
-                        while (!Thread.interrupted()) {
-                            float v = sensor.getVoltageBuffered();
-                            // Try to save some bandwidth by only updating if it matters.
-                            if (Math.abs(v - setv) > 0.1) {
-                                motorPwm.setDutyCycle(v / 3.3f);
-                                setv = v;
-                            }
-                            // Hooray for JITs; I hope they can code-motion this test out.
-                            if (cb != null) {
-                                cb.run(v);
-                            }
-                        }
-                    }
-                };
-            }
-        };
-    }
-}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOService.java b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOService.java
deleted file mode 100644 (file)
index 0ef8c85..0000000
+++ /dev/null
@@ -1,143 +0,0 @@
-// Based, in long past, on IOIOService example from Ytai's repository
-
-package com.acmetensortoys.android.teled;
-
-import android.app.Notification;
-import android.content.Intent;
-import android.os.IBinder;
-import android.os.Binder;
-import android.support.annotation.Nullable;
-import android.util.Log;
-import java.util.Vector;
-
-import ioio.lib.api.AnalogInput;
-import ioio.lib.api.Closeable;
-import ioio.lib.api.DigitalOutput;
-import ioio.lib.api.IOIO;
-import ioio.lib.api.PwmOutput;
-import ioio.lib.api.exception.ConnectionLostException;
-import ioio.lib.util.BaseIOIOLooper;
-import ioio.lib.util.IOIOLooper;
-
-public class IOIOService extends ioio.lib.util.android.IOIOService {
-    private static final int NOTIFY_ID = 1;
-    private IOIO ioio = null; // If we have one connected, here 'tis.
-    // TODO We should have some state that lets us identify which IOIO we mean
-    // which should be configurable through the UI.
-
-    public interface OnReady {
-        void onReady();
-        void onUnready();
-    }
-    private Vector<OnReady> subscribers = new Vector<>(5);
-
-    @Override
-    public IOIOLooper createIOIOLooper(final String type, Object info) {
-        if (type.equals("ioio.lib.android.bluetooth.BluetoothIOIOConnection")) {
-            Object[] info_ = (Object[])info;
-            Log.d("TDIS", "Looper creating for " + info_[0].toString() + ":" + info_[1].toString());
-        }
-
-        // TODO Check to see if it is the right IOIO.
-
-        return new BaseIOIOLooper() {
-            @Override
-            protected void setup() throws ConnectionLostException,
-                    InterruptedException {
-                synchronized(IOIOService.this) {
-                    if(ioio != null) {
-                        Log.e("TDIS", "Aaaaaaa! Setup with existing IOIO?", new Exception());
-                    }
-                    ioio = ioio_;
-                    for (OnReady l : subscribers) { l.onReady(); }
-                }
-            }
-
-            @Override
-            public void loop() throws ConnectionLostException,
-                    InterruptedException {
-                ioio.waitForDisconnect();
-            }
-
-            @Override
-            public void disconnected() {
-                synchronized(IOIOService.this) {
-                    for (OnReady l : subscribers) { l.onUnready(); }
-                    IOIOService.this.ioio = null;
-                }
-            }
-        };
-    }
-
-    final class LocalBinder extends Binder {
-        // XXX for development only
-        // IOIOService getService() { return IOIOService.this; }
-
-        // Subscribe or unsubscribe to the IOIO being connected;
-        // when we are ready, the behaviors offered below will work; when
-        // not, they won't.  On transition from ready to unready, all the
-        // threads will die of their own accord as their closed-over
-        // objects which reach into the IOIO issue exceptions.
-        public void addOnReady(OnReady l) {
-            subscribers.add(l);
-        }
-        public void removeOnReady(OnReady l) {
-            subscribers.remove(l);
-        }
-
-        @Nullable
-        public Thread makeBehaviorThread(IOIOBehaviors.Factory bf) {
-            IOIO ioio_;
-            // XXX? synchronized(IOIOService.this) {
-                ioio_ = ioio;
-            // }
-            if(ioio_ == null) {
-                return null;
-            }
-            final IOIOBehaviors.Factory.Behavior b;
-            final Vector<Closeable> cc = new Vector<>(5);
-            try {
-                b = bf.create(ioio_, cc);
-            } catch (Exception e) {
-                for(Closeable c : cc) { c.close(); }
-                return null;
-            }
-            return new Thread() {
-                public void run() {
-                    try {
-                        b.run();
-                    } catch (Exception e) {
-                        // ignored; the thread is allowed to just die;
-                        // anyone curious can use .isAlive() or .join().
-                        // They're also welcome to .interrupt()
-                    } finally {
-                        b.shutdown();
-                        for(Closeable c : cc) { c.close(); }
-                    }
-                }
-            };
-        }
-
-    }
-    private final IBinder mBinder = new LocalBinder();
-
-    @Override
-    public IBinder onBind(Intent arg0) {
-        Log.d("TDIS", "onBind");
-        Notification.Builder nb = new Notification.Builder(this);
-        Notification n = nb.setSmallIcon(R.drawable.ic_menu_send)
-                .setContentTitle("TeleD IOIO service running")
-                .setWhen(System.currentTimeMillis())
-                .setOngoing(true)
-                .build();
-        startForeground(NOTIFY_ID, n);
-        return mBinder;
-    }
-
-    @Override
-    public boolean onUnbind(Intent arg0) {
-        stopForeground(true);
-        stopSelf();
-        return false;
-    }
-}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/MainActivity.java b/mobile/src/main/java/com/acmetensortoys/android/teled/MainActivity.java
deleted file mode 100644 (file)
index 2f9109e..0000000
+++ /dev/null
@@ -1,176 +0,0 @@
-package com.acmetensortoys.android.teled;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.ComponentName;
-import android.os.IBinder;
-import android.os.Bundle;
-import android.renderscript.RSInvalidStateException;
-import android.support.design.widget.FloatingActionButton;
-import android.support.design.widget.Snackbar;
-import android.view.View;
-import android.support.design.widget.NavigationView;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v7.app.ActionBarDrawerToggle;
-import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.Toolbar;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.util.Log;
-
-public class MainActivity extends AppCompatActivity
-        implements NavigationView.OnNavigationItemSelectedListener {
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
-        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
-        setSupportActionBar(toolbar);
-
-        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
-        fab.setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
-                        .setAction("Action", null).show();
-            }
-        });
-
-        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
-        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
-                this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
-        drawer.setDrawerListener(toggle);
-        toggle.syncState();
-
-        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
-        navigationView.setNavigationItemSelectedListener(this);
-
-        Intent i = new Intent(this, IOIOService.class);
-        startService(i);                                    // start the IOIO service
-        bindService(i, tdisSC, Context.BIND_AUTO_CREATE);   // and get a handle to it, eventually
-    }
-
-    @Override
-    public void onBackPressed() {
-        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
-        if (drawer.isDrawerOpen(GravityCompat.START)) {
-            drawer.closeDrawer(GravityCompat.START);
-        } else {
-            super.onBackPressed();
-        }
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        // Inflate the menu; this adds items to the action bar if it is present.
-        getMenuInflater().inflate(R.menu.menu_scrolling, menu);
-        return true;
-    }
-
-    @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
-        // Handle action bar item clicks here. The action bar will
-        // automatically handle clicks on the Home/Up button, so long
-        // as you specify a parent activity in AndroidManifest.xml.
-        int id = item.getItemId();
-
-        //noinspection SimplifiableIfStatement
-        if (id == R.id.action_settings) {
-            return true;
-        }
-
-        return super.onOptionsItemSelected(item);
-    }
-
-    private Thread blinker;
-    private void makeBlinker() {
-        blinker = tdis.makeBehaviorThread(IOIOBehaviors.makeBlinkLEDMany(500));
-        if(blinker != null) {
-            blinker.start();
-        } else {
-            Log.d("Main", "onReady unable to start blinker");
-        }
-    }
-
-    private IOIOService.LocalBinder tdis = null;
-    private IOIOService.OnReady tdisOnReady = new IOIOService.OnReady() {
-        public void onReady() {
-            Log.d("Main", "TDIS onReady");
-            if(blinker != null) {
-                throw new RSInvalidStateException("onReady with blinker");
-            }
-            makeBlinker();
-        }
-        public void onUnready() {
-            Log.d("Main", "TDIS onUnready");
-            blinker = null;
-        }
-    };
-    private final ServiceConnection tdisSC = new ServiceConnection() {
-        @Override
-        public void onServiceConnected(ComponentName cn, IBinder service) {
-            Log.d("Main", "TDIS conn");
-            tdis = ((IOIOService.LocalBinder) service);
-            tdis.addOnReady(tdisOnReady);
-        }
-        @Override
-        public void onServiceDisconnected(ComponentName cn) {
-            // Because we're binding to a local service, this should never happen to us
-            Log.d("Main", "TDIS discon");
-            tdis = null;
-            tdisOnReady.onUnready();
-        }
-    };
-
-    @SuppressWarnings("StatementWithEmptyBody")
-    @Override
-    public boolean onNavigationItemSelected(MenuItem item) {
-        // Handle navigation view item clicks here.
-        int id = item.getItemId();
-
-        if (id == R.id.nav_camera) {
-            // Handle the camera action
-        } else if (id == R.id.nav_gallery) {
-
-        } else if (id == R.id.nav_slideshow) {
-
-        } else if (id == R.id.nav_manage) {
-
-        } else if (id == R.id.nav_share) {
-            if(tdis != null) {
-                Log.d("Main", "Share w/ non-null...");
-                if(blinker == null) {
-                    makeBlinker();
-                } else {
-                    blinker.interrupt();
-                    blinker = null;
-                }
-            }
-        } else if (id == R.id.nav_send) {
-            Log.d("Main", "Send...");
-        }
-
-        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
-        drawer.closeDrawer(GravityCompat.START);
-        return true;
-    }
-
-    @Override
-    public void onDestroy(){
-        unbindService(tdisSC);
-        super.onDestroy();
-    }
-}
-
-/* TODO:
- *
- *  * A settings activity with SMS configuration, including authentication
- *    information and an enable button.
- *
- *  * A settings activity with Bluetooth configuration and maybe USB?
- *
- *  * Spawn the IOIO service on startup if it isn't and see it work.
- */
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/RemoteControlService.java b/mobile/src/main/java/com/acmetensortoys/android/teled/RemoteControlService.java
deleted file mode 100644 (file)
index a964915..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-package com.acmetensortoys.android.teled;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.Binder;
-import android.os.IBinder;
-import android.util.Log;
-
-import java.util.Vector;
-
-public class RemoteControlService extends Service {
-    private boolean remoteControlEnabled = false;
-    private Vector<Thread> ongoingRemoteBehaviors = new Vector<>(1);
-
-    private void removeOngoing(Thread t) {
-        synchronized(RemoteControlService.this) {
-            ongoingRemoteBehaviors.remove(t);
-        }
-    }
-
-    public interface OnCessation {
-        void run();
-    }
-
-    // Assumes that t has been started already
-    private void _addOngoing(final Thread t, final OnCessation l) {
-        Thread nt = new Thread() {
-            public void run() {
-                while(t.isAlive()) {
-                    //noinspection EmptyCatchBlock
-                    try { t.join(); } catch (InterruptedException e) { }
-                }
-                removeOngoing(t);
-                l.run();
-            }
-        };
-        nt.start();
-        synchronized(this) { ongoingRemoteBehaviors.add(t); }
-    }
-
-    private boolean startAndAddOngoing(Thread t, OnCessation l) {
-        if(!remoteControlEnabled) {
-            return false;
-        }
-        t.start();
-        _addOngoing(t, l);
-        return true;
-    }
-
-    // TODO Should have a similar set of functionality for behaviors that should
-    // resume at some later point.  Consider losing the IOIO connection but wanting
-    // to restart when it comes back.
-
-    class LocalBinder extends Binder {
-        public void stopRemoteControl() {
-            synchronized (this) {
-                remoteControlEnabled = false;
-                // Cause all outstanding behaviors to cease their activities
-                for (Thread t : ongoingRemoteBehaviors) {
-                    t.interrupt();
-                }
-            }
-        }
-    }
-    private final IBinder mBinder = new LocalBinder();
-
-    @Override
-    public IBinder onBind(Intent arg0) {
-        Log.d("RCS", "onBind");
-        return mBinder;
-    }
-
-    @Override
-    public boolean onUnbind(Intent arg0) {
-        Log.d("RCS", "onUnbind");
-        return false;
-    }
-}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/SMSRecv.java b/mobile/src/main/java/com/acmetensortoys/android/teled/SMSRecv.java
deleted file mode 100644 (file)
index 4a4b5b0..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.acmetensortoys.android.teled;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-public class SMSRecv extends BroadcastReceiver {
-    public SMSRecv() {
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        // TODO: This method is called when the BroadcastReceiver is receiving
-        // an Intent broadcast.
-        throw new UnsupportedOperationException("Not yet implemented");
-
-        /* NWF planning aloud here.
-         *
-         * We should check that we should even be paying attention to SMS;
-         * they may be disabled.
-         *
-         * We're going to check some authentication present in the incoming
-         * message -- source number, signed message hash, etc. -- and are then
-         * going to decode it in full.
-         *
-         * The message should contain a sequence number that we echo back,
-         * Since SMS's may reorder on the wire, let's just not support
-         * multiple outstanding messages; simpler on us this way...
-         */
-    }
-}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Service/EphemeralTeleDService.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Service/EphemeralTeleDService.java
new file mode 100644 (file)
index 0000000..ba052fc
--- /dev/null
@@ -0,0 +1,67 @@
+package com.acmetensortoys.android.teled.Service;
+
+import android.app.IntentService;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * An {@link IntentService} subclass for handling asynchronous task requests in
+ * a service on a separate handler thread.
+ *
+ * Used within TeleD to handle things like SMS dispatch via PendingIntent.
+ * What a pain in the ass.
+ */
+public class EphemeralTeleDService extends IntentService {
+    private static final String ACTION_SEND_LOCATION_SMS
+            = "com.acmetensortoys.android.teled.action.SEND_LOCATION_SMS";
+    private static final String ACTION_TEST
+            = "com.acmetensortoys.android.teled.action.TEST";
+
+    // TODO: Rename parameters
+    private static final String EXTRA_PARAM1 = "com.acmetensortoys.android.teled.extra.PARAM1";
+    private static final String EXTRA_PARAM2 = "com.acmetensortoys.android.teled.extra.PARAM2";
+
+    public EphemeralTeleDService() {
+        super("EphemeralTeleDService");
+    }
+
+    public static PendingIntent pendingSendLocationSMS(Context context, Uri data) {
+        Intent intent = new Intent(ACTION_SEND_LOCATION_SMS,
+                data, context, EphemeralTeleDService.class);
+        return PendingIntent.getService(context, 0, intent, 0);
+    }
+
+    public static PendingIntent pendingTest(Context context, Uri data) {
+        Intent intent = new Intent(ACTION_TEST, data, context, EphemeralTeleDService.class);
+        return PendingIntent.getService(context, 0, intent, 0);
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        if (intent != null) {
+            final String action = intent.getAction();
+            if (ACTION_TEST.equals(action)) {
+                Log.d("EphemeralTeleDService", "Test action: " + intent.toString() + " eb=" + intent.getExtras().toString());
+            } else if (ACTION_SEND_LOCATION_SMS.equals(action)) {
+                // final String param1 = intent.getStringExtra(EXTRA_PARAM1);
+                handleActionSendLocationSMS(intent);
+            } /* else if (ACTION_BAZ.equals(action)) {
+                final String param1 = intent.getStringExtra(EXTRA_PARAM1);
+                final String param2 = intent.getStringExtra(EXTRA_PARAM2);
+                handleActionBaz(param1, param2);
+            } */
+        }
+    }
+
+    /**
+     * Handle action Foo in the provided background thread with the provided
+     * parameters.
+     */
+    private void handleActionSendLocationSMS(Intent i) {
+        // TODO: Handle action Foo
+        throw new UnsupportedOperationException("Not yet implemented");
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Service/PositionReporting.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Service/PositionReporting.java
new file mode 100644 (file)
index 0000000..4be09a9
--- /dev/null
@@ -0,0 +1,18 @@
+package com.acmetensortoys.android.teled.Service;
+
+import android.Manifest;
+import android.content.Context;
+import android.location.LocationListener;
+import android.location.LocationManager;
+
+public abstract class PositionReporting
+{
+    public PositionReporting(Context ctx, LocationListener ll) {
+        LocationManager lm = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
+
+        ctx.enforceCallingOrSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION,
+                "Missing location permission");
+
+        lm.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 5000, 10, ll);
+    }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Service/SMSRecv.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Service/SMSRecv.java
new file mode 100644 (file)
index 0000000..04841d2
--- /dev/null
@@ -0,0 +1,65 @@
+package com.acmetensortoys.android.teled.Service;
+
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.acmetensortoys.android.teled.Utils.BehaviorHandleSet;
+
+// TODO: The constructor of this class should bind the Service for us
+// so we're not binding and un-binding at every message?  What a pain in the ass.
+
+public class SMSRecv extends BroadcastReceiver {
+    public SMSRecv() {
+        Log.d("SMSRecv","Constructor");
+    }
+
+    @Override
+    public void onReceive(final Context context, Intent intent) {
+        Log.d("TeleD/SMSRecv", intent.toString());
+
+        // TODO: Check that we should be doing SMS, authenticate incoming message,
+        // figure out what behaviors need to be spawned, ...
+
+        context.bindService(
+                new Intent(context, TeleDService.class),
+                new ServiceConnection() {
+                    @Override
+                    public void onServiceConnected(ComponentName name, IBinder service) {
+                        TeleDService.LocalBinder tds = ((TeleDService.LocalBinder) service);
+
+                        // Grab list of current behaviors
+                        StringBuilder sb = new StringBuilder();
+                        BehaviorHandleSet.showMetaMap(sb, tds.getActiveBehaviors());
+
+                        // TODO: Send response message
+
+                        context.unbindService(this);
+                    }
+
+                    @Override
+                    public void onServiceDisconnected(ComponentName name) { ; }
+                },
+                0);
+
+        /* NWF planning aloud here.
+         *
+         * We should check that we should even be paying attention to SMS;
+         * they may be disabled.  That's now handled by dynamic registration of
+         * the receiver in TeleDService.
+         *
+         * We're going to check some authentication present in the incoming
+         * message -- source number, signed message hash, etc. -- and are then
+         * going to decode it in full.
+         *
+         * The message should contain a sequence number that we echo back,
+         * Since SMS's may reorder on the wire, let's just not support
+         * multiple outstanding messages; simpler on us this way...
+         */
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Service/TeleDService.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Service/TeleDService.java
new file mode 100644 (file)
index 0000000..6a25be7
--- /dev/null
@@ -0,0 +1,202 @@
+package com.acmetensortoys.android.teled.Service;
+
+import android.Manifest;
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.os.Binder;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.acmetensortoys.android.teled.IOIO.BehaviorFactory;
+import com.acmetensortoys.android.teled.IOIO.TeleDIOIOManager;
+import com.acmetensortoys.android.teled.R;
+import com.acmetensortoys.android.teled.Utils.BehaviorHandle;
+import com.acmetensortoys.android.teled.Utils.BehaviorHandleSet;
+import com.acmetensortoys.android.teled.Utils.Subscribee;
+import com.acmetensortoys.android.teled.Utils.SubscribeeImpl;
+import com.acmetensortoys.android.teled.Utils.SuspendableBH;
+
+import java.util.Map;
+
+import fj.function.Effect1;
+import ioio.lib.util.android.IOIOAndroidApplicationHelper;
+
+/*
+ * This is the core persistent object of the system (or at least, that's the idea);
+ * this service should know how to respond to messages from the UI,
+ * any remote control channels, location sources, and the underlying
+ * behavioral hardware.  Phfew!
+ */
+public class TeleDService extends Service {
+    // Manage a collection of behaviors
+    private final BehaviorHandleSet<BehaviorHandle<?>> bhs = new BehaviorHandleSet();
+
+    // Some behaviors pause and unpause on SMS preference or GPS preference (or both!)
+    private final BehaviorHandleSet<SuspendableBH<?>> smsbhs = new BehaviorHandleSet<>();
+    private final BehaviorHandleSet<SuspendableBH<?>> gpsbhs = new BehaviorHandleSet<>();
+
+    // Manage an IOIO
+    private final TeleDIOIOManager ios = new TeleDIOIOManager();
+    private final IOIOAndroidApplicationHelper ioh = new IOIOAndroidApplicationHelper(this, ios);
+    private boolean ioactive;
+    private void enableIOIO(Boolean on) {
+        synchronized(ioh) {
+            if (on && !ioactive) {
+                ioh.restart();
+            } else if (!on && ioactive) {
+                ioh.stop();
+            }
+            ioactive = on;
+        }
+    }
+    private void enableIOIOByPreference() {
+        enableSMS(PreferenceManager
+                .getDefaultSharedPreferences(this)
+                .getBoolean("ioio_ena", false));
+    }
+
+    // Manage a Broadcast Receiver for SMSes
+    //
+    // XXX Should this really be here or should it be part of the main application?
+    // it's moved back and forth a few times.
+    private final SubscribeeImpl<Boolean> mSMSStatSubee = new SubscribeeImpl<>(false);
+    private final SMSRecv mSMS = new SMSRecv();
+    private void enableSMS(Boolean on) {
+        synchronized(mSMS) {
+            if (on && !mSMSStatSubee.getLast()) {
+                mSMSStatSubee.publish(true);
+                registerReceiver(mSMS, new IntentFilter(Manifest.permission.RECEIVE_SMS));
+            } else if (!on && mSMSStatSubee.getLast()) {
+                unregisterReceiver(mSMS);
+                mSMSStatSubee.publish(false);
+            }
+        }
+    }
+    private void enableSMSByPreference() {
+        enableSMS(PreferenceManager
+                .getDefaultSharedPreferences(this)
+                .getBoolean("sms_ena", false));
+    }
+
+    // Export an API
+    public class LocalBinder extends Binder {
+        public Subscribee<Boolean> getIOIOStatus() { return ios.getStatus(); }
+        public <In> BehaviorHandle<In> addIOIOBehavior(
+                BehaviorHandleSet.Metadata meta,
+                BehaviorFactory<In> bf) {
+            return addActiveBehavior(meta, ios.makeBehaviorThread(bf));
+        }
+
+        public <In> BehaviorHandle<In>
+        addActiveBehavior(BehaviorHandleSet.Metadata meta, BehaviorHandle<In> bh) {
+            bhs.addActiveBehavior(meta, bh);
+            return bh;
+        }
+
+        public BehaviorHandle<?> getActiveBehaviorById(int id) {
+            return bhs.getBehaviorById(id);
+        }
+        public Map<Integer,BehaviorHandleSet.Metadata>
+        getActiveBehaviors() {
+            return bhs.getActiveBehaviorMeta();
+        }
+
+        // are both SMS and GPS enabled?
+        public boolean sendSMSPositionReports() {
+            return mSMSStatSubee.getLast() /* && mGPSStatSubee.getLast() */ ;
+        }
+
+        // XXX Any need for this?
+        public Subscribee<Boolean> getSMSStatus() { return mSMSStatSubee; }
+    }
+    private final LocalBinder mBinder = new LocalBinder();
+
+    // Android preferences event glue
+    private final SharedPreferences.OnSharedPreferenceChangeListener ospcl =
+            new SharedPreferences.OnSharedPreferenceChangeListener() {
+                @Override
+                public void
+                onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+                                          String key) {
+                    if (sharedPreferences != PreferenceManager
+                            .getDefaultSharedPreferences(TeleDService.this)) {
+                        // Some other collection of preferences changing?  How odd.
+                        return;
+                    }
+                    switch(key) {
+                        case "ioio_ena": enableIOIOByPreference(); break;
+                        case "sms_ena": enableSMSByPreference(); break;
+                    }
+                }
+            };
+
+    // Android service-management glue
+    private static final int NOTIFY_ID = 1;
+
+    @Override
+    public IBinder onBind(Intent arg0) {
+        Log.d("TDIS", "onBind");
+
+        // This is a little hackish; to simplify the start/stop/restart state machine,
+        // just always start and immediately disable if preferences indicate.
+        ioh.start();
+        ioactive = true;
+        enableIOIOByPreference();
+
+        enableSMSByPreference();
+
+        PreferenceManager.
+                getDefaultSharedPreferences(this).
+                registerOnSharedPreferenceChangeListener(ospcl);
+
+        Notification.Builder notif = new Notification.Builder(this);
+        notif.setSmallIcon(R.drawable.ic_menu_send)
+                .setContentTitle("TeleD service running")
+                .setWhen(System.currentTimeMillis())
+                .setOngoing(true);
+        startForeground(NOTIFY_ID, notif.build());
+
+        // TODO Hook a callback for displaying SMS enabled status?
+
+        // TODO Hook a callback to update notification contents
+        bhs.getActiveBehaviorSubee().subscribe(new Effect1<Map<Integer,BehaviorHandleSet.Metadata>>() {
+            public void f(Map<Integer,BehaviorHandleSet.Metadata> x) {
+                StringBuilder sb = new StringBuilder("Update to active behaviors:\n");
+                BehaviorHandleSet.showMetaMap(sb, x);
+                Log.d("TDIS", sb.toString());
+            }
+        });
+
+        return mBinder;
+    }
+
+    @Override
+    public boolean onUnbind(Intent arg0) {
+        PreferenceManager.
+                getDefaultSharedPreferences(this).
+                registerOnSharedPreferenceChangeListener(ospcl);
+
+        enableSMS(false);
+        enableIOIO(false);
+
+        stopForeground(true);
+        return false;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        ioh.create();
+    }
+
+    @Override
+    public void onDestroy() {
+        ioh.destroy();
+        super.onDestroy();
+    }
+
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/UI/FragPrefs.java b/mobile/src/main/java/com/acmetensortoys/android/teled/UI/FragPrefs.java
new file mode 100644 (file)
index 0000000..fc3cc15
--- /dev/null
@@ -0,0 +1,65 @@
+package com.acmetensortoys.android.teled.UI;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import com.acmetensortoys.android.teled.R;
+
+public class FragPrefs
+        extends PreferenceFragment {
+
+    private final int REQID_SMS_PICK = 1234;
+
+    @Override
+    public void onCreate(Bundle sis) {
+        super.onCreate(sis);
+        addPreferencesFromResource(R.xml.pref_frag_main);
+
+        /* Customize preferences */
+        Preference p;
+
+        /*
+         * Bahaha.  We can't create a custom Preference class for this because...
+         * getPreferenceManager().getFragment() is package private.  Goddammit.
+         */
+        p = findPreference("sms_pick");
+        p.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference _p) {
+                Intent conPik = new Intent(
+                        Intent.ACTION_PICK,
+                        ContactsContract.Contacts.CONTENT_URI);
+                startActivityForResult(conPik, REQID_SMS_PICK);
+                return true;
+            }
+        });
+
+        /*
+        p = findPreference("ioio_mac");
+        p.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference _p) {
+                ListPreference lp = (ListPreference) _p;
+                lp.setEntries(new CharSequence[]{});
+                lp.setEntryValues(new CharSequence[]{});
+                return true;
+            }
+        });
+        */
+    }
+
+    @Override
+    public void onActivityResult(int req, int res, Intent i) {
+        if(req == REQID_SMS_PICK && res == Activity.RESULT_OK) {
+            Log.d("FragPrefs", i.toString());
+        }
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/UI/MainActivity.java b/mobile/src/main/java/com/acmetensortoys/android/teled/UI/MainActivity.java
new file mode 100644 (file)
index 0000000..96a5563
--- /dev/null
@@ -0,0 +1,312 @@
+package com.acmetensortoys.android.teled.UI;
+
+import com.acmetensortoys.android.teled.R;
+import com.acmetensortoys.android.teled.Service.EphemeralTeleDService;
+import com.acmetensortoys.android.teled.Service.SMSRecv;
+import com.acmetensortoys.android.teled.Utils.BehaviorHandle;
+import com.acmetensortoys.android.teled.IOIO.BehaviorFactories;
+import com.acmetensortoys.android.teled.Service.TeleDService;
+import com.acmetensortoys.android.teled.Utils.BehaviorHandleSet;
+import com.acmetensortoys.android.teled.Utils.SubscribeeImpl;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.ComponentName;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.renderscript.RSInvalidStateException;
+import android.support.annotation.NonNull;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.util.Log;
+
+import fj.function.Effect1;
+
+public class MainActivity extends Activity
+        implements SharedPreferences.OnSharedPreferenceChangeListener
+    /*
+        implements NavigationView.OnNavigationItemSelectedListener
+    */
+{
+
+    private final static int PERM_REQ_IX_RECV_SMS = 1;
+    private final static int PERM_REQ_IX_FINE_LOC = 2;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        PreferenceManager
+                .getDefaultSharedPreferences(this)
+                .registerOnSharedPreferenceChangeListener(this);
+
+        setContentView(R.layout.newmain);
+
+        getFragmentManager().beginTransaction()
+                .add(R.id.main_fragment_container,new FragPrefs())
+                .commit();
+
+        /*
+        setContentView(R.layout.fragment_main);
+
+        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        */
+
+        /*
+        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+        fab.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
+                        .setAction("Action", null).show();
+            }
+        });
+
+        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
+        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
+                this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
+        drawer.setDrawerListener(toggle);
+        toggle.syncState();
+
+        NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
+        navigationView.setNavigationItemSelectedListener(this);
+        */
+    }
+
+    @Override
+    public void onDestroy(){
+        if (tds != null) { unbindService(tdisSC); }
+
+        PreferenceManager
+                .getDefaultSharedPreferences(this)
+                .unregisterOnSharedPreferenceChangeListener(this);
+
+        super.onDestroy();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        bindService(new Intent(this, TeleDService.class), tdisSC,
+                Context.BIND_AUTO_CREATE | Context.BIND_ABOVE_CLIENT);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // Inflate the menu; this adds items to the action bar if it is present.
+        getMenuInflater().inflate(R.menu.menu_scrolling, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        // Handle action bar item clicks here. The action bar will
+        // automatically handle clicks on the Home/Up button, so long
+        // as you specify a parent activity in AndroidManifest.xml.
+        int id = item.getItemId();
+
+        //noinspection SimplifiableIfStatement
+        if (id == R.id.action_settings) {
+            return true;
+        }
+
+        return super.onOptionsItemSelected(item);
+    }
+
+    private BehaviorHandle blinker;
+    private void makeBlinker() {
+        blinker = tds.addIOIOBehavior(
+                new BehaviorHandleSet.Metadata("BlinkMany"),
+                BehaviorFactories.makeBlinkLEDMany(500));
+        if(blinker != null) {
+            blinker.start();
+        } else {
+            Log.d("Main", "unable to start blinker");
+        }
+    }
+
+    private TeleDService.LocalBinder tds = null;
+    private final Effect1<Boolean> tdiOnReady = new Effect1<Boolean>() {
+        public void f(Boolean b) {
+            if(b) {
+                Log.d("Main", "TDI onReady");
+                if (blinker != null) {
+                    throw new RSInvalidStateException("onReady with blinker");
+                }
+                makeBlinker();
+            } else {
+                Log.d("Main", "TDI onUnready");
+                blinker.interrupt();
+                try { blinker.join(); } catch (InterruptedException e) { ; }
+                blinker = null;
+            }
+        }
+    };
+
+    private final ServiceConnection tdisSC = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName cn, IBinder service) {
+            Log.d("Main", "TDIS conn");
+            tds = ((TeleDService.LocalBinder) service);
+            tds.getIOIOStatus().subscribe(tdiOnReady);
+        }
+        @Override
+        public void onServiceDisconnected(ComponentName cn) {
+            // Because we're binding to a local service, this should never happen to us
+            Log.d("Main", "TDIS discon");
+            tds = null;
+            tdiOnReady.f(false);
+        }
+    };
+
+    /*
+    @SuppressWarnings("StatementWithEmptyBody")
+    @Override
+    public boolean onNavigationItemSelected(MenuItem item) {
+        // Handle navigation view item clicks here.
+        int id = item.getItemId();
+
+        if (id == R.id.nav_camera) {
+            Log.d("Main", "Camera...");
+
+            if(tds != null) {
+                Map<Integer,String> m = tds.getActiveBehaviors();
+                for (Map.Entry<Integer,String> x : m.entrySet()) {
+                    BehaviorHandle<?> b = tds.getActiveBehaviorById(x.getKey());
+                    if(b != null) {
+                        Log.d("Main", "Stopping behavior ID " + x.getKey().toString());
+                        b.interrupt();
+                        try { b.join(); } catch (Exception e) { break; }
+                    }
+                }
+            }
+        } else if (id == R.id.nav_gallery) {
+            if(tds != null) {
+                Log.d("Main", "Share w/ non-null...");
+                if(blinker == null) {
+                    makeBlinker();
+                } else {
+                    blinker.interrupt();
+                    blinker = null;
+                }
+            }
+        } else if (id == R.id.nav_slideshow) {
+            if(tds != null) {
+            }
+        } else if (id == R.id.nav_manage) {
+            if(tds != null) {
+                // tds.enableIOIO(true);
+                BehaviorHandle<Void> x = tds.addActiveBehavior("test",
+                        ThreadBehaviorHandle.create(new Runnable() {
+                            public void run() {
+                                while(true) {
+                                    try {
+                                        Thread.sleep(5000);
+                                    } catch (Exception e) {
+                                        return;
+                                    }
+                                    Log.d("TestBehavior", "Tick");
+                                }
+                            }
+                        }));
+                x.start();
+            }
+        } else if (id == R.id.nav_share) {
+            Log.d("Main", "Share...");
+
+            if(checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION)
+                    != PackageManager.PERMISSION_GRANTED)
+            {
+                Log.d("Main", "Requesting permission?");
+                requestPermissions(
+                        new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
+                        PERM_REQ_IX_FINE_LOC);
+            } else {
+                Log.d("Main", "Have permission?");
+
+                LocationManager lm = (LocationManager) getSystemService(LOCATION_SERVICE);
+                Log.d("Main", lm.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER).toString());
+            }
+        } else if (id == R.id.nav_send) {
+            Log.d("Main", "Send...");
+
+            if(checkSelfPermission(Manifest.permission.RECEIVE_SMS)
+                != PackageManager.PERMISSION_GRANTED)
+            {
+                Log.d("Main", "Requesting permission?");
+                requestPermissions(
+                        new String[]{Manifest.permission.RECEIVE_SMS},
+                        PERM_REQ_IX_RECV_SMS);
+            } else {
+                Log.d("Main", "Have permission?");
+                if(tds != null) {
+                    tds.enableSMS(true);
+                }
+            }
+        }
+
+        DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
+        drawer.closeDrawer(GravityCompat.START);
+        return true;
+    }
+    */
+
+    @Override
+    public void onRequestPermissionsResult(int rq, @NonNull String[] perms, @NonNull int[] grs) {
+        Log.d("Main", "ORPR:" + perms[0] + ":" + Integer.toString(grs[0]));
+        //if(rq == PERM_REQ_IX_RECV_SMS && grs[0] == PackageManager.PERMISSION_GRANTED) {
+        //}
+    }
+
+    // XXX Remove?
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences p, String k) {
+        switch(k) {
+            case "sms_ena": break;
+            case "gps_ena": break;
+            case "test":
+                AlarmManager am = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+                PendingIntent pi = EphemeralTeleDService.pendingTest(this,Uri.EMPTY);
+                if(PreferenceManager
+                        .getDefaultSharedPreferences(this)
+                        .getBoolean("test", false)) {
+                    Log.d("Main", "test OSPC enabled");
+                    am.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+                            SystemClock.elapsedRealtime(), 1000 * 60, pi);
+                    try { pi.send(); } catch (PendingIntent.CanceledException ce) {
+                        Log.d("Main", "pi cancel?");
+                    }
+                } else {
+                    Log.d("Main", "test OSPC disabled");
+                    am.cancel(pi);
+                    pi.cancel();
+                }
+            break;
+            default:
+                Log.d("MainActivity", "Unknown preference changed:" + k);
+                break;
+        }
+    }
+}
+
+/* TODO:
+ *
+ *  * A settings activity with SMS configuration, including authentication
+ *    information and an enable button.
+ *
+ *  * A settings activity with Bluetooth configuration and maybe USB?
+ *
+ *  * Spawn the IOIO service on startup if it isn't and see it work.
+ */
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/LocationUpdateBH.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/LocationUpdateBH.java
new file mode 100644 (file)
index 0000000..9b1ae60
--- /dev/null
@@ -0,0 +1,37 @@
+package com.acmetensortoys.android.teled.Utils.Android;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.location.LocationManager;
+
+public class LocationUpdateBH extends PendingIntentBH {
+    private final Context c;
+    private final String prov;
+    private final long mint;
+    private final float mind;
+
+    LocationUpdateBH(Context c, String provider, long minTime, float minDist, PendingIntent pi){
+        super(pi);
+        this.c = c;
+        this.prov = provider;
+        this.mint = minTime;
+        this.mind = minDist;
+    }
+
+    @Override
+    protected boolean onStart(PendingIntent pi) {
+        LocationManager lm = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE);
+        try {
+            lm.requestLocationUpdates(prov, mint, mind, pi);
+            return true;
+        } catch (SecurityException se) {
+            return false;
+        }
+    }
+
+    @Override
+    protected void onPause(PendingIntent pi) {
+        LocationManager lm = (LocationManager) c.getSystemService(Context.LOCATION_SERVICE);
+        lm.removeUpdates(pi);
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/PendingIntentBH.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/PendingIntentBH.java
new file mode 100644 (file)
index 0000000..fda15d9
--- /dev/null
@@ -0,0 +1,100 @@
+package com.acmetensortoys.android.teled.Utils.Android;
+
+import android.app.PendingIntent;
+
+import com.acmetensortoys.android.teled.Utils.Subscribee;
+import com.acmetensortoys.android.teled.Utils.SubscribeeImpl;
+import com.acmetensortoys.android.teled.Utils.SuspendableBH;
+
+import fj.Void;
+import fj.function.Effect1;
+
+public abstract class PendingIntentBH extends SuspendableBH<Void> {
+
+    // or restart; true to start, false to indicate a desire to stay suspended
+    abstract protected boolean onStart(PendingIntent pi);
+
+    // or stop; must be idempotent
+    abstract protected void onPause(PendingIntent pi);
+
+    public PendingIntentBH(PendingIntent pi) {
+        ln = new SubscribeeImpl<>(BehaviorState.NEW);
+        this.pi = pi;
+    }
+
+    //
+    // You can stop reading here if all you want to know is what to extend.
+    //
+
+    final private SubscribeeImpl<BehaviorState> ln;
+    final private PendingIntent pi;
+
+    private void die() {
+        synchronized(this) {
+            ln.publish(BehaviorState.DEAD);
+            this.notifyAll();
+        }
+    }
+
+    @Override
+    public final void start() {
+        BehaviorState bs = ln.getLast();
+        if(bs == BehaviorState.NEW || bs == BehaviorState.SUSPENDED) {
+            if(this.onStart(pi)) {
+                ln.publish(BehaviorState.RUNNING);
+            } else if (bs == BehaviorState.NEW) {
+                ln.publish(BehaviorState.SUSPENDED);
+            }
+        }
+    }
+
+    @Override
+    public void suspend() {
+        BehaviorState bs = ln.getLast();
+        switch(ln.getLast()) {
+            case RUNNING:
+                this.onPause(pi);
+                // fallthru
+            case NEW:
+                ln.publish(BehaviorState.SUSPENDED);
+                break;
+            case SUSPENDED:
+            case DEAD:
+                break;
+        }
+    }
+
+    @Override
+    public final void interrupt() {
+        switch(ln.getLast()) {
+            case NEW:
+            case RUNNING:
+                this.onPause(pi);
+            case SUSPENDED:
+                die();
+                break;
+            case DEAD:
+                break;
+        }
+        pi.cancel();
+    }
+
+    @Override
+    public final void join() throws InterruptedException {
+        synchronized (this) {
+            while (ln.getLast() != BehaviorState.DEAD) {
+                this.wait();
+            }
+        }
+    }
+
+    @Override
+    public final Subscribee<BehaviorState> getStateSubscribee() {
+        return ln;
+    }
+
+    @Override
+    public final Effect1<Void> getUpdateIn() {
+        return null;
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/RecurrentAlarmBH.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Android/RecurrentAlarmBH.java
new file mode 100644 (file)
index 0000000..f2d83df
--- /dev/null
@@ -0,0 +1,33 @@
+package com.acmetensortoys.android.teled.Utils.Android;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+
+public class RecurrentAlarmBH extends PendingIntentBH {
+    private final Context c;
+    private final int type;
+    private final long tam;
+    private final long ri;
+
+    RecurrentAlarmBH(Context c, int type, long triggerAtMillis, long repeatInterval, PendingIntent pi){
+        super(pi);
+        this.c = c;
+        this.type = type;
+        this.tam = triggerAtMillis;
+        this.ri = repeatInterval;
+    }
+
+    @Override
+    protected boolean onStart(PendingIntent pi) {
+        AlarmManager am = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
+        am.setRepeating(type, tam, ri, pi);
+        return true;
+    }
+
+    @Override
+    protected void onPause(PendingIntent pi) {
+        AlarmManager am = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
+        am.cancel(pi);
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/BehaviorHandle.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/BehaviorHandle.java
new file mode 100644 (file)
index 0000000..6e923da
--- /dev/null
@@ -0,0 +1,14 @@
+package com.acmetensortoys.android.teled.Utils;
+
+import fj.function.Effect1;
+
+public abstract class BehaviorHandle<In> {
+    public enum BehaviorState { NEW, RUNNING, SUSPENDED, DEAD };
+
+    abstract public void start(); // move from NEW to RUNNING
+    abstract public void interrupt(); // request RUNNING to DEAD transition
+    abstract public void join() throws InterruptedException; // await status DEAD
+    abstract public Subscribee<BehaviorState> getStateSubscribee(); // to be notified on any state transition
+
+    abstract public Effect1<In> getUpdateIn();
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/BehaviorHandleSet.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/BehaviorHandleSet.java
new file mode 100644 (file)
index 0000000..3282c83
--- /dev/null
@@ -0,0 +1,90 @@
+package com.acmetensortoys.android.teled.Utils;
+
+import android.graphics.drawable.Icon;
+
+import com.google.common.collect.HashBiMap;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import fj.function.Effect1;
+
+// Manage a collection of active behavior threads; removal from the collection
+// is handled by the behaviors themselves when they transition.
+//
+// Each behavior is also given a unique integer identifier for remote reference.
+public class BehaviorHandleSet<BH extends BehaviorHandle<?>> {
+    public static class Metadata {
+        final public String name;
+        final public Icon icon;
+
+        public Metadata (String name) {
+            this.name = name;
+            this.icon = null;
+        }
+        public Metadata (String name, Icon icon) {
+            this.name = name;
+            this.icon = icon;
+        }
+    }
+
+    private int nextId = 0;
+    private final HashBiMap<BH,Integer> activeBehaviorIds = HashBiMap.create();
+    private final HashMap<Integer,Metadata> activeBehaviorMeta = new HashMap<>();
+    private final SubscribeeImpl<Map<Integer,Metadata>> abmSubee
+            = new SubscribeeImpl<Map<Integer,Metadata>>(activeBehaviorMeta);
+
+    public void
+    addActiveBehavior(final Metadata meta, final BH bh)
+    {
+        synchronized(this) {
+            while (activeBehaviorIds.containsValue(nextId)) { nextId++; }
+            activeBehaviorIds.put(bh, nextId);
+            activeBehaviorMeta.put(nextId, meta);
+            abmSubee.publish(getActiveBehaviorMeta());
+            bh.getStateSubscribee().subscribe(new Effect1<BehaviorHandle.BehaviorState>() {
+                public void f(BehaviorHandle.BehaviorState x) {
+                    if (x == BehaviorHandle.BehaviorState.DEAD) {
+                        synchronized(this) {
+                            int oldId = activeBehaviorIds.remove(bh);
+                            activeBehaviorMeta.remove(oldId);
+                            abmSubee.publish(getActiveBehaviorMeta());
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    public BH
+    getBehaviorById(int id) {
+        synchronized(this) { return activeBehaviorIds.inverse().get(id); }
+    }
+
+    public Iterator<BH>
+    getBehaviors() {
+        return activeBehaviorIds.keySet().iterator();
+    }
+
+    // Clone the behavior meta map for consumption of the outside world.
+    @SuppressWarnings("unchecked")
+    public Map<Integer,Metadata>
+    getActiveBehaviorMeta() {
+        synchronized (this) {
+            return (Map<Integer,Metadata>)(activeBehaviorMeta.clone());
+        }
+    }
+
+    public Subscribee<Map<Integer,Metadata>>
+    getActiveBehaviorSubee() { return abmSubee; }
+
+    public static void showMetaMap(StringBuilder b, Map<Integer,Metadata> m) {
+        for(Map.Entry<Integer,Metadata> x : m.entrySet()) {
+            b.append(x.getKey());
+            b.append(" ");
+            b.append(x.getValue().name);
+            b.append("\n");
+        }
+    }
+}
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Subscribee.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/Subscribee.java
new file mode 100644 (file)
index 0000000..5e97856
--- /dev/null
@@ -0,0 +1,9 @@
+package com.acmetensortoys.android.teled.Utils;
+
+import fj.function.Effect1;
+
+public interface Subscribee<V> {
+    V getLast();
+    void subscribe(Effect1<V> cb);
+    void unsubscribe(Effect1<V> cb);
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/SubscribeeImpl.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/SubscribeeImpl.java
new file mode 100644 (file)
index 0000000..9c8858c
--- /dev/null
@@ -0,0 +1,34 @@
+package com.acmetensortoys.android.teled.Utils;
+
+import java.util.LinkedList;
+import java.util.List;
+import fj.function.Effect1;
+
+public class SubscribeeImpl<V> implements Subscribee<V> {
+    private V last;
+    private final List<Effect1<V>> subscribers = new LinkedList<>();
+
+    public SubscribeeImpl(V init) { last = init; }
+
+    public void publish(V b) {
+        synchronized (this) {
+            last = b;
+            for (Effect1<V> l : subscribers) {
+                l.f(b);
+            }
+        }
+    }
+    public V getLast() {
+        return last;
+    }
+    public void subscribe(Effect1<V> l) {
+        synchronized (this) {
+            subscribers.add(l);
+        }
+    }
+    public void unsubscribe(Effect1<V> l) {
+        synchronized (this) {
+            subscribers.remove(l);
+        }
+    }
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/SuspendableBH.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/SuspendableBH.java
new file mode 100644 (file)
index 0000000..0596227
--- /dev/null
@@ -0,0 +1,6 @@
+package com.acmetensortoys.android.teled.Utils;
+
+public abstract class SuspendableBH<In> extends BehaviorHandle<In> {
+    abstract public void suspend(); // request transition from RUNNING to SUSPENDED
+    // use start() to resume.
+}
\ No newline at end of file
diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/ThreadBehaviorHandle.java b/mobile/src/main/java/com/acmetensortoys/android/teled/Utils/ThreadBehaviorHandle.java
new file mode 100644 (file)
index 0000000..6b8e00f
--- /dev/null
@@ -0,0 +1,50 @@
+package com.acmetensortoys.android.teled.Utils;
+
+import android.util.Log;
+
+import fj.Void;
+import fj.function.Effect1;
+
+public class ThreadBehaviorHandle<In> extends BehaviorHandle<In> {
+    private final Thread thread;
+    private final Effect1<In> updateIn;
+    /* Actual liveness transitions are expected to be managed by the thread t */
+    final private Subscribee<BehaviorState> ln;
+
+    public ThreadBehaviorHandle(Thread t, Effect1<In> u, Subscribee<BehaviorState> ln) {
+        this.thread = t;
+        this.updateIn = u;
+        this.ln = ln;
+    }
+
+    /* Given a runnable and an updater, make a behavior handle */
+    public static <In> ThreadBehaviorHandle<In> create(final Runnable r, Effect1<In> u) {
+        final SubscribeeImpl<BehaviorHandle.BehaviorState> lni
+                = new SubscribeeImpl<>(BehaviorHandle.BehaviorState.NEW);
+        Thread t = new Thread() {
+            public void run() {
+                lni.publish(BehaviorState.RUNNING);
+                r.run();
+                lni.publish(BehaviorState.DEAD);
+            }
+        };
+        return new ThreadBehaviorHandle<In>(t, u, lni);
+    }
+
+    /* Sometimes we don't even need an updater */
+    public static ThreadBehaviorHandle<Void> create(final Runnable r) {
+        return create(r, new Effect1<Void>() { public void f(Void v) { v.absurd(); } });
+    }
+
+    @Override
+    public void join() throws InterruptedException { thread.join(); }
+    @Override
+    public void interrupt() { thread.interrupt(); }
+    @Override
+    public void start() { thread.start(); }
+
+    @Override
+    public Effect1<In> getUpdateIn() { return updateIn; }
+    @Override
+    public Subscribee<BehaviorState> getStateSubscribee() { return ln; }
+}
\ No newline at end of file
diff --git a/mobile/src/main/res/drawable/ic_info_black_24dp.xml b/mobile/src/main/res/drawable/ic_info_black_24dp.xml
new file mode 100644 (file)
index 0000000..34b8202
--- /dev/null
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z" />
+</vector>
diff --git a/mobile/src/main/res/drawable/ic_notifications_black_24dp.xml b/mobile/src/main/res/drawable/ic_notifications_black_24dp.xml
new file mode 100644 (file)
index 0000000..e3400cf
--- /dev/null
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M11.5,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zm6.5,-6v-5.5c0,-3.07 -2.13,-5.64 -5,-6.32V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v0.68c-2.87,0.68 -5,3.25 -5,6.32V16l-2,2v1h17v-1l-2,-2z" />
+</vector>
diff --git a/mobile/src/main/res/drawable/ic_sync_black_24dp.xml b/mobile/src/main/res/drawable/ic_sync_black_24dp.xml
new file mode 100644 (file)
index 0000000..5a283aa
--- /dev/null
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01,-.25 1.97,-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0,-4.42,-3.58,-8,-8,-8zm0 14c-3.31 0,-6,-2.69,-6,-6 0,-1.01.25,-1.97.7,-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4,-4,-4,-4v3z" />
+</vector>
\ No newline at end of file
index e00e4f7e16d6f6d5e98383238e2797f940980519..bec9ee1d51a4a1182665f7e99be8d3a95dcebb61 100644 (file)
@@ -5,7 +5,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true"
-    tools:context="com.acmetensortoys.android.teled.MainActivity">
+    tools:context=".UI.MainActivity">
 
     <android.support.design.widget.AppBarLayout
         android:layout_width="match_parent"
index 5a22ef51a0a44f16eba7f1a40f538287a6a0d8d3..be9a35f53313f93e53e2c1e1d02f8400c973b3c3 100644 (file)
@@ -9,7 +9,7 @@
     android:paddingRight="@dimen/activity_horizontal_margin"
     android:paddingTop="@dimen/activity_vertical_margin"
     app:layout_behavior="@string/appbar_scrolling_view_behavior"
-    tools:context="com.acmetensortoys.android.teled.MainActivity"
+    tools:context=".UI.MainActivity"
     tools:showIn="@layout/app_bar_main">
 
     <TextView
diff --git a/mobile/src/main/res/layout/fragment_main.xml b/mobile/src/main/res/layout/fragment_main.xml
new file mode 100644 (file)
index 0000000..399ec3e
--- /dev/null
@@ -0,0 +1,49 @@
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".UI.ExampleFragment">
+
+    <android.support.design.widget.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="@style/AppTheme.AppBarOverlay">
+
+        <android.support.v7.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            android:background="?attr/colorPrimary"
+            app:popupTheme="@style/AppTheme.PopupOverlay"
+            />
+
+    </android.support.design.widget.AppBarLayout>
+
+    <LinearLayout
+        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"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        tools:showIn="@layout/app_bar_main"
+        android:orientation="vertical"
+        android:weightSum="1">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="27dp"
+            android:text="@string/frag_main_head" />
+
+        <Switch
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:text="SMS Reception Enabled"
+            android:id="@+id/main_frag_sms_ena"
+            android:checked="false" />
+
+    </LinearLayout>
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/mobile/src/main/res/layout/newmain.xml b/mobile/src/main/res/layout/newmain.xml
new file mode 100644 (file)
index 0000000..3fb22f9
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".UI.MainActivity">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@+id/main_fragment_container">
+
+    </RelativeLayout>
+
+</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/mobile/src/main/res/values-v21/styles.xml b/mobile/src/main/res/values-v21/styles.xml
deleted file mode 100644 (file)
index 251fb9f..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-<resources>>
-
-    <style name="AppTheme.NoActionBar">
-        <item name="windowActionBar">false</item>
-        <item name="windowNoTitle">true</item>
-        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
-        <item name="android:statusBarColor">@android:color/transparent</item>
-    </style>
-</resources>
diff --git a/mobile/src/main/res/values-w820dp/dimens.xml b/mobile/src/main/res/values-w820dp/dimens.xml
deleted file mode 100644 (file)
index 63fc816..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-<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>
index 3bc506667954e324ba40bea4c1453717895d9bb8..66ca8cd738f73ec2d726b0d77e4c790795ca0968 100644 (file)
@@ -5,4 +5,7 @@
     <string name="navigation_drawer_close">Close navigation drawer</string>
 
     <string name="action_settings">Settings</string>
+    <string name="title_activity_settings">Settings</string>
+
+    <string name="frag_main_head">Acme Tensor Toys Demo</string>
 </resources>
index 545b9c6d2c45df7938f8ffd5ac2d806dec36f122..497a8c7f9213fb9f1569d22f5f7fca5aa5dd4abf 100644 (file)
@@ -17,4 +17,4 @@
 
     <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
 
-</resources>
+</resources>
\ No newline at end of file
diff --git a/mobile/src/main/res/xml/pref_frag_main.xml b/mobile/src/main/res/xml/pref_frag_main.xml
new file mode 100644 (file)
index 0000000..be91c63
--- /dev/null
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <CheckBoxPreference
+        android:id="@+id/frag_pref_test"
+        android:key="test"
+        android:title="Test" />
+
+    <PreferenceCategory android:title="SMS">
+
+        <CheckBoxPreference
+            android:id="@+id/frag_pref_sms_ena"
+            android:key="sms_ena"
+            android:title="Enabled"
+            />
+
+        <PreferenceScreen
+            android:id="@+id/frag_pref_sms_pick"
+            android:key="sms_pick"
+            android:title="Pick contact">
+        </PreferenceScreen>
+
+    </PreferenceCategory>
+
+    <PreferenceCategory android:title="Position Reporting">
+
+    <CheckBoxPreference
+        android:id="@+id/frag_pref_gps_ena"
+        android:key="gps_ena"
+        android:title="Enabled"
+        />
+
+    </PreferenceCategory>
+
+    <PreferenceCategory android:title="IOIO">
+
+        <CheckBoxPreference
+            android:id="@+id/frag_pref_ioio_ena"
+            android:key="ioio_ena"
+            android:title="Enabled"
+            />
+
+        <!--
+        <ListPreference
+            android:id="@+id/frag_pref_ioio_mac"
+            android:key="ioio_mac"
+            android:title="Bluetooth MAC Address"
+            />
+            -->
+
+    </PreferenceCategory>
+
+</PreferenceScreen>
\ No newline at end of file
index e616b6817b60c1fadde93193701359272e43dd38..987fdc3d1b1ebdec00cd88ed37e84f5dc3212792 100644 (file)
@@ -2,11 +2,11 @@ apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 23
-    buildToolsVersion "23.0.2"
+    buildToolsVersion "23.0.3"
 
     defaultConfig {
         applicationId "com.acmetensortoys.android.teled"
-        minSdkVersion 21
+        minSdkVersion 23
         targetSdkVersion 23
         versionCode 1
         versionName "1.0"
@@ -35,5 +35,5 @@ dependencies {
 dependencies {
     compile fileTree(dir: 'libs', include: ['*.jar'])
     compile 'com.google.android.support:wearable:+'
-    compile 'com.google.android.gms:play-services-wearable:+'
+    // compile 'com.google.android.gms:play-services-wearable:+'
 }
index 27d6296f8251f45f79b2987f4591bccc56b4ab99..124a92391a201eb28383365483a58b803e51fa2d 100644 (file)
@@ -5,7 +5,7 @@
     android:id="@+id/container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="com.acmetensortoys.android.teled.MainActivity"
+    tools:context="com.acmetensortoys.android.teled.UI.MainActivity"
     tools:deviceIds="wear">
 
     <TextView