From bf527e34e3defae003a9412575a04eaf84275e4f Mon Sep 17 00:00:00 2001 From: Nathaniel Wesley Filardo Date: Mon, 8 Feb 2016 05:11:31 -0500 Subject: [PATCH] Some hack and slash New behavior API for IOIOService; I kind of like it better. Push IOIO behavior factories out to their own class to reduce clutter. Tidy a bunch of Android warnings. Explicitly delcare desire to have Java 1.7 Skeletal remote-control tracking service. --- mobile/build.gradle | 5 + mobile/src/main/AndroidManifest.xml | 13 +- .../android/teled/IOIOBehaviors.java | 105 ++++++++++ .../android/teled/IOIOService.java | 143 ++++++++++++++ .../android/teled/MainActivity.java | 45 ++++- .../android/teled/RemoteControlService.java | 78 ++++++++ .../android/teled/TeleDIOIOService.java | 184 ------------------ wear/build.gradle | 6 +- 8 files changed, 383 insertions(+), 196 deletions(-) create mode 100644 mobile/src/main/java/com/acmetensortoys/android/teled/IOIOBehaviors.java create mode 100644 mobile/src/main/java/com/acmetensortoys/android/teled/IOIOService.java create mode 100644 mobile/src/main/java/com/acmetensortoys/android/teled/RemoteControlService.java delete mode 100644 mobile/src/main/java/com/acmetensortoys/android/teled/TeleDIOIOService.java diff --git a/mobile/build.gradle b/mobile/build.gradle index 416738e..94efc61 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -17,6 +17,11 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } } // IOIO support diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 201b22f..8dbc759 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -4,7 +4,7 @@ - + @@ -20,20 +20,23 @@ android:theme="@style/AppTheme.NoActionBar"> - + android:name=".IOIOService" + android:enabled="true" /> + + + android:permission="android.permission.RECEIVE_SMS"> diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOBehaviors.java b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOBehaviors.java new file mode 100644 index 0000000..2293ae3 --- /dev/null +++ b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOBehaviors.java @@ -0,0 +1,105 @@ +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 cc) throws Exception; + } + + static private Factory makeBlinkOnce(final int pinId, final int dur) { + return new Factory() { + public Behavior create(IOIO ioio_, Collection 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 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 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 new file mode 100644 index 0000000..0ef8c85 --- /dev/null +++ b/mobile/src/main/java/com/acmetensortoys/android/teled/IOIOService.java @@ -0,0 +1,143 @@ +// 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 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 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 index d96a71d..2f9109e 100644 --- a/mobile/src/main/java/com/acmetensortoys/android/teled/MainActivity.java +++ b/mobile/src/main/java/com/acmetensortoys/android/teled/MainActivity.java @@ -6,6 +6,7 @@ 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; @@ -46,6 +47,10 @@ public class MainActivity extends AppCompatActivity 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 @@ -80,17 +85,43 @@ public class MainActivity extends AppCompatActivity return super.onOptionsItemSelected(item); } - private TeleDIOIOService tdis = null; + 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 = ((TeleDIOIOService.LocalBinder) service).getService(); + 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(); } }; @@ -111,13 +142,15 @@ public class MainActivity extends AppCompatActivity } else if (id == R.id.nav_share) { if(tdis != null) { Log.d("Main", "Share w/ non-null..."); - tdis.setForceFeedbackBehavior(true); + if(blinker == null) { + makeBlinker(); + } else { + blinker.interrupt(); + blinker = null; + } } } else if (id == R.id.nav_send) { - Intent i = new Intent(this, TeleDIOIOService.class); Log.d("Main", "Send..."); - startService(i); - bindService(i, tdisSC, Context.BIND_AUTO_CREATE); } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); diff --git a/mobile/src/main/java/com/acmetensortoys/android/teled/RemoteControlService.java b/mobile/src/main/java/com/acmetensortoys/android/teled/RemoteControlService.java new file mode 100644 index 0000000..a964915 --- /dev/null +++ b/mobile/src/main/java/com/acmetensortoys/android/teled/RemoteControlService.java @@ -0,0 +1,78 @@ +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 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/TeleDIOIOService.java b/mobile/src/main/java/com/acmetensortoys/android/teled/TeleDIOIOService.java deleted file mode 100644 index 5d34acc..0000000 --- a/mobile/src/main/java/com/acmetensortoys/android/teled/TeleDIOIOService.java +++ /dev/null @@ -1,184 +0,0 @@ -// Based on IOIOService example from ytai's repository - -package com.acmetensortoys.android.teled; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Intent; -import android.os.IBinder; -import android.os.Binder; -import android.util.Log; - -import ioio.lib.api.AnalogInput; -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; -import ioio.lib.util.android.IOIOService; - -/** - * An example IOIO service. While this service is alive, it will attempt to - * connect to a IOIO and blink the LED. A notification will appear on the - * notification bar, enabling the user to stop the service. - */ -public class TeleDIOIOService extends IOIOService { - private static final int NOTIFY_ID = 0; - - - /* - public abstract class Command { - abstract public void behave() - throws ConnectionLostException, InterruptedException; - }; - - - public class BlinkOnce extends Command { - private int duration; - public BlinkOnce(int dur) { duration = dur; } - public void behave() - throws ConnectionLostException, InterruptedException { - led_.write(true); - Thread.sleep(duration); - led_.write(false); - } - } - - private LinkedBlockingQueue ioioq; - */ - - private boolean forceFeedbackBehavior = false; - private synchronized void awaitAnyBehavior() { - while(!forceFeedbackBehavior) { - try { - this.wait(); - } catch (InterruptedException e) { - // ignored - } - } - } - - public void setForceFeedbackBehavior(boolean nv) { - Log.d("TDIS", "sffb"); - if(forceFeedbackBehavior && !nv) { - synchronized (this) { - forceFeedbackBehavior = false; - } - } else if (!forceFeedbackBehavior && nv) { - synchronized(this) { - forceFeedbackBehavior = true; - this.notifyAll(); - } - } - } - - @Override - public IOIOLooper createIOIOLooper(String type, Object info) { - return new BaseIOIOLooper() { - private DigitalOutput boardLED; - - private AnalogInput forceSensor1; - private long forceSensor1dix; - - // XXX Warmer controls? - - // XXX This is not how it will be in the final version - private DigitalOutput motorEnable; - private PwmOutput motorPwm; - - @Override - protected void setup() throws ConnectionLostException, - InterruptedException { - Log.d("TDIS", "Looper setup"); - - boardLED = ioio_.openDigitalOutput(IOIO.LED_PIN, false); - - forceSensor1 = ioio_.openAnalogInput(31); - forceSensor1.setBuffer(50); - - motorEnable = ioio_.openDigitalOutput(1,false); // active low - motorPwm = ioio_.openPwmOutput(2,100); - - forceSensor1dix = 0; - - Log.d("TDIS", "Looper setup finish"); - } - - @Override - public void loop() throws ConnectionLostException, - InterruptedException { - - awaitAnyBehavior(); - if (forceFeedbackBehavior) { - boolean did = forceSensor1.available() > 0; - float v = 0.0f; - while (forceSensor1.available() > 0) { - v = forceSensor1.getVoltageBuffered(); - forceSensor1dix++; - } - if (did) { - motorPwm.setDutyCycle(v / 3.3f); - } - } - } - - @Override - public void disconnected() { } - }; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log.d("TDIS", "onStartCommand"); - - int result = super.onStartCommand(intent, flags, startId); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - if (intent != null && intent.getAction() != null - && intent.getAction().equals("stop")) { - // User clicked the notification. Need to stop the service. - Log.d("TDIS", "stopping..."); - - setForceFeedbackBehavior(false); - - // nm.cancel(0); - // stopSelf(); - } else { - Notification.Builder nb = new Notification.Builder(this); - - nm.notify(NOTIFY_ID, - nb.setContentIntent(PendingIntent.getService(this, 0, new Intent( - "stop", null, this, this.getClass()), 0)) - .setSmallIcon(R.drawable.ic_menu_send) - .setContentTitle("TeleD IOIO service running") - .setWhen(System.currentTimeMillis()) - .setOngoing(true) - .build()); - } - return result; - } - - protected final class LocalBinder extends Binder { - TeleDIOIOService getService() { return TeleDIOIOService.this; } - } - private final IBinder mBinder = new LocalBinder(); - - @Override - public IBinder onBind(Intent arg0) { - Log.d("TDIS", "onBind"); - return mBinder; - } - - - - - /* - public void doNotify() { - try { - //ioioq.put(new BlinkOnce(500)); - } catch (InterruptedException e) { - } - } - */ -} \ No newline at end of file diff --git a/wear/build.gradle b/wear/build.gradle index 327aad2..e616b68 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' - android { compileSdkVersion 23 buildToolsVersion "23.0.2" @@ -18,6 +17,11 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } } // IOIO support -- 2.50.1