Add EpmController class.
diff --git a/src/com/projectara/araepm/EpmController.java b/src/com/projectara/araepm/EpmController.java
new file mode 100644
index 0000000..4923d7e
--- /dev/null
+++ b/src/com/projectara/araepm/EpmController.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2014 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.projectara.araepm;
+
+import java.io.IOException;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+
+import static android.content.Context.I2C_SERVICE;
+
+import android.hardware.I2cManager;
+
+public class EpmController {
+
+    // EPM mailbox semantics:
+    //
+    // - Bit[31]: set this to indicate need to change attach state.
+    //            (If this is already set, a state change is ongoing.
+    //            This shouldn't happen under ordinary circumstances.)
+    //
+    // - Bit[30]: if set, attach the EPM. If unset, detach the EPM.
+    //
+    // - Bits[7:0]: port to change attach state of.
+    //
+    // After executing the EPM action, the SVC will clear bit 31 and
+    // put a result code into Bits[15:0], which is zero on success.
+
+    // How long to wait for an EPM state change (milliseconds).
+    private static final long TIMEOUT_MS = 10000;
+
+    // The EPM mailbox UniPro attribute.
+    private static final DME.AttributeId MBOX = DME.AttributeId.DME_MAILBOX;
+
+    // The application processort port
+    //
+    // FIXME we should read this from the DME so we know where we are.
+    // Is there a way? Does it matter?
+    private static final int HACK_MY_PORT = 0;
+
+    private static final int MAX_ENDO_PORT = 0xff;
+    private static final int MIN_ENDO_PORT = 0;
+
+    private static final String TAG = "araepm";
+
+    public static enum EpmState {
+        EPM_ATTACH,
+        EPM_DETACH;
+    }
+
+    public static enum EpmChangeStatus {
+        EPM_OK,
+        EPM_ERROR,
+        EPM_TIMEOUT;
+    }
+
+    public interface EpmChangeCallback {
+        public void onEpmChangeFinished(EpmState state,
+                                        EpmChangeStatus status,
+                                        int port);
+    }
+
+    private DME dme;
+    private EpmChangeCallback callback;
+
+    private HandlerThread handlerThread;
+    private Handler handler;
+
+    public EpmController(Context context,
+                         EpmChangeCallback callback) {
+        I2cManager i2c = (I2cManager)context.getSystemService(I2C_SERVICE);
+        this.dme = new DME(i2c);
+        this.callback = callback;
+    }
+
+    private boolean isStartedLocked() {
+        return handlerThread != null;
+    }
+
+    private void ensureStartedLocked() {
+        if (!isStartedLocked()) {
+            throw new IllegalStateException("not yet started");
+        }
+    }
+
+    public synchronized void start() {
+        if (isStartedLocked()) {
+            throw new IllegalStateException("already started");
+        }
+        handlerThread = new HandlerThread("EpmController");
+        handlerThread.start();
+        Looper looper = handlerThread.getLooper();
+        if (looper == null) {
+            Log.wtf(TAG, "can't get handler thread looper");
+            throw new RuntimeException();
+        }
+        handler = new Handler(looper);
+    }
+
+    public synchronized void stop() {
+        if (!isStartedLocked()) {
+            throw new IllegalStateException("not yet started");
+        }
+        handlerThread.quit();
+        while (handlerThread.isAlive())
+            ;
+        handler = null;
+    }
+
+    private void doAttachDetachLocked(int port, EpmState desired) {
+        if (port > MAX_ENDO_PORT || port < MIN_ENDO_PORT) {
+            Log.wtf(TAG, "Invalid port value: " + port);
+            callback.onEpmChangeFinished(desired, EpmChangeStatus.EPM_ERROR,
+                                         port);
+            return;
+        }
+
+        ensureStartedLocked();
+        handler.post(new EpmStateChanger(port, desired));
+    }
+
+    public synchronized void attachEpm(int port) {
+        doAttachDetachLocked(port, EpmState.EPM_ATTACH);
+    }
+
+    public synchronized void detachEpm(int port) {
+        doAttachDetachLocked(port, EpmState.EPM_DETACH);
+    }
+
+    private class EpmStateChanger implements Runnable {
+        private final int port;
+        private final EpmState desired;
+        private final int mBoxValue;
+
+        private static final int S_IDX = 0; // selector index
+        private static final int CHANGING_BIT = 31;
+        private static final int ATTACH_BIT = 30;
+        private static final int PORT_MASK = 0xff;
+        private static final int RESULT_MASK = 0xffff;
+
+        private static final boolean PEER_MBOX = true;
+
+        public EpmStateChanger(int port, EpmState desired) {
+            this.port = port;
+            this.desired = desired;
+            this.mBoxValue = ((1 << CHANGING_BIT) |
+                              ((desired == EpmState.EPM_ATTACH ? 1 : 0)
+                               << ATTACH_BIT) |
+                              (port & PORT_MASK));
+        }
+
+        private int getMBox() throws IOException {
+            return dme.readDMEConfig(HACK_MY_PORT, PEER_MBOX, MBOX, S_IDX, 0);
+        }
+
+        private boolean isEpmChangeOngoing(int mbox) {
+            return (mbox & (1 << CHANGING_BIT)) != 0;
+        }
+
+        private void startStateChange() throws IOException {
+            dme.writeDMEConfig(HACK_MY_PORT, PEER_MBOX, MBOX, S_IDX, mBoxValue,
+                               0);
+        }
+
+        private void finish(EpmChangeStatus status) {
+            callback.onEpmChangeFinished(desired, status, port);
+        }
+
+        @Override
+        public void run() {
+            long start = SystemClock.elapsedRealtime();
+            boolean started = false;
+
+            // FIXME try/finally a wake lock here -- this is a bad
+            // time to go to sleep
+            while (true) {
+                int mbox;
+
+                // Bail on timeout.
+                if (SystemClock.elapsedRealtime() - start > TIMEOUT_MS) {
+                    finish(EpmChangeStatus.EPM_TIMEOUT);
+                    return;
+                }
+
+                // Check the mailbox.
+                try {
+                    mbox = getMBox();
+                } catch (IOException ioe) {
+                    Log.e(TAG, "can't read EPM mailbox: " + ioe);
+                    finish(EpmChangeStatus.EPM_ERROR);
+                    return;
+                }
+
+                // Try to start the EPM state change, allowing for
+                // a previously ongoing state change to continue.
+                if (!started) {
+                    if (!isEpmChangeOngoing(mbox)) {
+                        try {
+                            startStateChange();
+                        } catch (IOException ioe) {
+                            Log.e(TAG, "can't request EPM change: " + ioe);
+                            finish(EpmChangeStatus.EPM_ERROR);
+                            return;
+                        }
+                        started = true;
+                    }
+                    continue;
+                }
+
+                // Wait for the change to finish, and relay the result
+                // to the callback.
+                if (isEpmChangeOngoing(mbox)) {
+                    continue;
+                }
+                int exitStatus = mbox & RESULT_MASK;
+                finish(exitStatus == 0 ?
+                       EpmChangeStatus.EPM_OK :
+                       EpmChangeStatus.EPM_ERROR);
+                return;
+            }
+        }
+    }
+}