Merge "Launch managed provisioning for secondary users"
diff --git a/Android.mk b/Android.mk
index 429fc81..3645a91 100644
--- a/Android.mk
+++ b/Android.mk
@@ -3,12 +3,16 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 \
+    ManagedProvisioningComm
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
 
+LOCAL_PROTOC_OPTIMIZE_TYPE := nano
+LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/
+
 LOCAL_PACKAGE_NAME := ManagedProvisioning
 LOCAL_CERTIFICATE := platform
 LOCAL_PRIVILEGED_MODULE := true
@@ -17,3 +21,4 @@
 
 include $(BUILD_PACKAGE)
 
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 816e6a7..9a11441 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -55,7 +55,8 @@
 
     <application
         android:allowClearUserData="false"
-        android:hardwareAccelerated="true">
+        android:hardwareAccelerated="true"
+        android:usesCleartextTraffic="false">
 
         <!--
              Note: Setup activities are android:immersive to prevent full-screen notifications (USB
diff --git a/comm/Android.mk b/comm/Android.mk
new file mode 100644
index 0000000..70005b5
--- /dev/null
+++ b/comm/Android.mk
@@ -0,0 +1,31 @@
+#
+# Copyright (C) 2015 The Android Open Source Project
+#
+# 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.
+#
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/
+LOCAL_PROTOC_OPTIMIZE_TYPE := nano
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src) \
+    $(call all-proto-files-under, protos)
+
+LOCAL_MODULE := ManagedProvisioningComm
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/comm/protos/bluetooth.proto b/comm/protos/bluetooth.proto
new file mode 100644
index 0000000..2536b88
--- /dev/null
+++ b/comm/protos/bluetooth.proto
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.
+ */
+
+syntax = "proto2";
+
+package bluetooth;
+
+option java_package = "com.android.managedprovisioning.comm";
+option optimize_for = LITE_RUNTIME;
+
+// Provisioning status updates
+message StatusUpdate {
+  optional int32 status_code = 1;
+  optional string custom_data = 2;
+}
+
+// Information about the connecting device
+message DeviceInfo {
+  optional int32 api_version = 1;
+  optional string make = 2;
+  optional string model = 3;
+  optional string serial = 4;
+  optional string fingerprint = 5;
+  optional int64 totalMemory = 6;
+  optional int32 screenWidthPx = 7;
+  optional int32 screenHeightPx = 8;
+  optional float screenDensity = 9;
+}
+
+// Holds network data transferred over Bluetooth.
+message NetworkData {
+  enum Status {
+    OK = 1; // Data is valid.
+    EOF = 2; // End of file reached.
+    SHUTDOWN = 3; // Connection ending; shut down all threads
+  }
+  optional int32 connection_id = 1;
+  optional Status status = 2 [default = OK];
+  optional bytes data = 3;
+}
+
+// Data packet sent and received over Bluetooth. The deviceIdentifier should
+// always be set when sending a request. Only one of the other packets should
+// be set.
+message CommPacket {
+  optional string deviceIdentifier = 1;
+  optional StatusUpdate status_update = 2;
+  optional NetworkData network_data = 3;
+  optional DeviceInfo  device_info = 4;
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/BluetoothAcceptor.java b/comm/src/com/android/managedprovisioning/comm/BluetoothAcceptor.java
new file mode 100644
index 0000000..b0b75f3
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/BluetoothAcceptor.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import android.bluetooth.BluetoothAdapter;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * An Acceptor is a thread that will loop over the ServerSocketWrapper
+ * and accept connections.  It will use the handler factory to spin up
+ * a Handler for each of those connections.
+ */
+public class BluetoothAcceptor extends Thread implements ProvisioningAcceptor {
+    /**
+     * Number of consecutive Bluetooth IOExceptions allowed before trying to recreate the
+     * Bluetooth server socket.
+     */
+    private static final int IO_EXCEPTION_RECREATE = 5;
+
+    private final ServerSocketWrapper mServerSocket;
+
+    private volatile boolean mIsRunning;
+    private int mConsecutiveFails;
+    private boolean mDoRecreate;
+
+    /** User defined callback. */
+    private final StatusCallback mCallback;
+
+    /** Main thread handler. Used to post callback events. */
+    private final Handler mHandler;
+
+    /**
+     * Synchronized set of device ids expected to connect.
+     * @see #listenForDevice(String)
+     * @see #stopListening(String)
+     */
+    private final Set<String> mExpectedDevices;
+
+    /**
+     * Create a new instance that communicates over Bluetooth. The {@link #startConnection()}
+     * method must be called before communication can begin.
+     * @param adapter Bluetooth adapter used to establish a connection
+     * @param serviceName name of the created Bluetooth service, used for discovery
+     * @param uuid unique identifier of the created Bluetooth service, used for discovery
+     * @param callback callback that receives information about connected devices
+     */
+    public BluetoothAcceptor(BluetoothAdapter adapter, String serviceName, UUID uuid,
+            StatusCallback callback) {
+        mExpectedDevices = Collections.synchronizedSet(new HashSet<String>());
+        // Setup callback
+        mCallback = callback;
+        mHandler = new Handler(Looper.getMainLooper());
+        // Create socket
+        mServerSocket = new BluetoothServerSocketWrapper(serviceName, uuid, adapter);
+    }
+
+    @Override
+    public void run() {
+        mIsRunning = true;
+        try {
+            while (mIsRunning) {
+                try {
+                    SocketWrapper socket = mServerSocket.accept();
+                    handleConnection(socket);
+                    mConsecutiveFails = 0;
+                } catch (IOException e) {
+                    ProvisionCommLogger.logd(e);
+                    ++mConsecutiveFails;
+                    if (mIsRunning && (mConsecutiveFails > IO_EXCEPTION_RECREATE || mDoRecreate)) {
+                        mDoRecreate = false;
+                        try {
+                            mServerSocket.recreate();
+                        } catch (IOException e1) {
+                            ProvisionCommLogger.loge("Problem recreating server socket", e1);
+                        }
+                    }
+                }
+            }
+        } finally {
+            close();
+        }
+    }
+
+    @Override
+    public boolean isInProgress() {
+        return mIsRunning;
+    }
+
+    private void close() {
+        if (mServerSocket != null) {
+            try {
+                ProvisionCommLogger.logd("Closing acceptor acceptance task");
+                mIsRunning = false;
+                mServerSocket.close();
+            } catch (Exception e) {
+                ProvisionCommLogger.logd(e);
+            }
+        }
+    }
+
+    @Override
+    public synchronized void startConnection() throws IOException {
+        mServerSocket.recreate();
+        if (!mIsRunning) {
+            start();
+            mIsRunning = true;
+        }
+    }
+
+    @Override
+    public void stopConnection() {
+        close();
+    }
+
+    @Override
+    public void listenForDevice(String deviceIdentifier) {
+        mExpectedDevices.add(deviceIdentifier);
+    }
+
+    @Override
+    public void stopListening(String deviceIdentifier) {
+        mExpectedDevices.remove(deviceIdentifier);
+    }
+
+    /**
+     * Handle Bluetooth socket connection on a new thread.
+     * @param socket the Bluetooth connection
+     */
+    private void handleConnection(SocketWrapper socket) {
+        new ProxyConnectionHandler(new Channel(socket), mHandler, mCallback,
+                Collections.unmodifiableSet(mExpectedDevices)).start();
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/BluetoothServerSocketWrapper.java b/comm/src/com/android/managedprovisioning/comm/BluetoothServerSocketWrapper.java
new file mode 100644
index 0000000..234ac3e
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/BluetoothServerSocketWrapper.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothServerSocket;
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * Wrapper around a {@link BluetoothServerSocket}.
+ */
+public class BluetoothServerSocketWrapper implements ServerSocketWrapper {
+    private final BluetoothAdapter mBtAdapter;
+    private final UUID mUuid;
+    private final String mServerName;
+    private BluetoothServerSocket mServerSocket;
+
+    /**
+     * Start listening for Bluetooth connections.
+     * @param serverName the name of server; used for Bluetooth Service Discovery Protocol.
+     * @param uuid unique identifier for the Service Discovery Protocol record.
+     * @param adapter Bluetooth adapter used for listening
+     * @throws NullPointerException if either {@code uuid} or {@code adapter} are null.
+     * @throws IllegalArgumentException if {@code serverName} is either {@code null} or empty.
+     */
+    public BluetoothServerSocketWrapper(String serverName, UUID uuid,
+            BluetoothAdapter adapter) {
+        if (uuid == null || adapter == null) {
+            throw new NullPointerException("UUID and BluetoothAdapter cannot be null");
+        }
+        if (TextUtils.isEmpty(serverName)) {
+            throw new IllegalArgumentException("serverName cannot be empty");
+        }
+        mServerName = serverName;
+        mBtAdapter = adapter;
+        mUuid = uuid;
+    }
+
+    @Override
+    public SocketWrapper accept() throws IOException {
+        return new BluetoothSocketWrapper(mBtAdapter, mServerSocket.accept());
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (mServerSocket != null) {
+            mServerSocket.close();
+        }
+    }
+
+    @Override
+    public void recreate() throws IOException {
+        try {
+            close();
+        } catch (Exception e) {
+            ProvisionCommLogger.loge(e);
+        }
+        mServerSocket = mBtAdapter.listenUsingInsecureRfcommWithServiceRecord(mServerName, mUuid);
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/BluetoothSocketWrapper.java b/comm/src/com/android/managedprovisioning/comm/BluetoothSocketWrapper.java
new file mode 100644
index 0000000..eb30f08
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/BluetoothSocketWrapper.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothSocket;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.UUID;
+
+/**
+ * Provides a testable wrapper around a {@code BluetoothSocket}.
+ */
+public class BluetoothSocketWrapper implements SocketWrapper {
+    private final BluetoothAdapter mBluetoothAdapter;
+    private BluetoothSocket mSocket;
+    private String mMacAddress;
+    private UUID mUuid;
+
+    // Used by BluetoothServerSocket when socket exists.
+    public BluetoothSocketWrapper(BluetoothAdapter adapter, BluetoothSocket socket) {
+        mBluetoothAdapter = adapter;
+        mSocket = socket;
+    }
+
+    // Used for clients so that a ReliableChannel can recreate the connection.
+    public BluetoothSocketWrapper(BluetoothAdapter adapter, String macAddress, String uuid) {
+        mBluetoothAdapter = adapter;
+        mMacAddress = macAddress;
+        mUuid = UUID.fromString(uuid);
+    }
+
+    @Override
+    public InputStream getInputStream() throws IOException {
+        return mSocket.getInputStream();
+    }
+
+    @Override
+    public OutputStream getOutputStream() throws IOException {
+        return mSocket.getOutputStream();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (mSocket != null) {
+            try {
+                getInputStream().close();
+            } catch (IOException ex) {
+                ProvisionCommLogger.logw(ex);
+            }
+            try {
+                getOutputStream().close();
+            } catch (IOException ex) {
+                ProvisionCommLogger.logw(ex);
+            }
+            try {
+                mSocket.close();
+            } catch (IOException ex) {
+                ProvisionCommLogger.logw(ex);
+            }
+        }
+    }
+
+    @Override
+    public boolean isConnected() {
+        return mSocket != null && mSocket.isConnected();
+    }
+
+    @Override
+    public void open() throws IOException {
+        if (mMacAddress != null) {
+            BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(mMacAddress);
+            mSocket = device.createInsecureRfcommSocketToServiceRecord(mUuid);
+        }
+        if (mBluetoothAdapter.isDiscovering()) {
+            mBluetoothAdapter.cancelDiscovery();
+        }
+        mSocket.connect();
+    }
+
+    @Override
+    public String getIdentifier() {
+        return mSocket.getRemoteDevice().getAddress();
+    }
+
+    @Override
+    public void recreate() throws IOException {
+        if (mMacAddress == null) {
+            throw new IOException("Cannot recreate a socket with no MAC Address");
+        }
+        try {
+            close();
+        } catch (IOException e) {
+
+        }
+        open();
+    }
+
+    public void setReconnectUuid(String mBluetoothUuid) {
+        mUuid = UUID.fromString(mBluetoothUuid);
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/Channel.java b/comm/src/com/android/managedprovisioning/comm/Channel.java
new file mode 100644
index 0000000..2d33ea2
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/Channel.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import com.android.managedprovisioning.comm.Bluetooth;
+import com.android.managedprovisioning.comm.Bluetooth.CommPacket;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import com.google.protobuf.nano.CodedInputByteBufferNano;
+import com.google.protobuf.nano.CodedOutputByteBufferNano;
+
+/**
+ * A Channel allows communication over a SocketWrapper using Protobuf messages.
+ */
+public class Channel implements AutoCloseable {
+    protected final SocketWrapper mSocket;
+
+    // Used to synchronize writes while allowing simultaneous read/writing.
+    protected final Object mWriteLock = new Object();
+
+    public Channel(SocketWrapper socket) {
+        mSocket = socket;
+    }
+
+    // GuardedBy(mWriteLock)
+    public void write(CommPacket packet)
+            throws IOException {
+        synchronized (mWriteLock) {
+            OutputStream outputStream = mSocket.getOutputStream();
+
+            int size = packet.getSerializedSize();
+            ProvisionCommLogger.logd("Sending message size: " + size);
+            int delimitSize = CodedOutputByteBufferNano.computeRawVarint32Size(size);
+            byte[] array = new byte[size + delimitSize];
+            CodedOutputByteBufferNano outputBuffer = CodedOutputByteBufferNano.newInstance(array);
+            outputBuffer.writeRawVarint32(size);
+            packet.writeTo(outputBuffer);
+            if (outputBuffer.spaceLeft() != 0) {
+                throw new IOException("Incorrect size calculated");
+            }
+            outputStream.write(array);
+
+            outputStream.flush();
+        }
+    }
+
+    public synchronized CommPacket read() throws IOException {
+        return read(mSocket.getInputStream());
+    }
+
+    @SuppressWarnings("unchecked")
+    protected synchronized CommPacket read(InputStream inputStream) throws IOException {
+        CodedInputByteBufferNano inputBuffer = readByteBuffer(inputStream);
+        try {
+            return readPacket(inputBuffer);
+        } catch (ClassCastException e) {
+            ProvisionCommLogger.loge("Incorrect type called for return value", e);
+            return null;
+        }
+    }
+
+    protected CodedInputByteBufferNano readByteBuffer(InputStream inputStream) throws IOException {
+        byte[] readBuffer = new byte[512];
+        int index = 0;
+        // Read bytes while the most significant bit is set.  The CodedInputByteBufferNano from
+        // proto-nano only reads up to 10 bytes, so we will do the same.
+        do {
+            while (inputStream.read(readBuffer, index, 1) <= 0);
+        } while ((readBuffer[index++] < 0) && (index < 10));
+
+        CodedInputByteBufferNano inputBuffer =
+                CodedInputByteBufferNano.newInstance(readBuffer, 0, index);
+        int size = inputBuffer.readRawVarint32();
+        byte[] buffer = new byte[size];
+        int readIndex = 0;
+        while (readIndex < size) {
+            int amount = inputStream.read(buffer, readIndex, size - readIndex);
+            if (amount > 0) {
+                readIndex += amount;
+            }
+        }
+        return CodedInputByteBufferNano.newInstance(buffer);
+    }
+
+    protected CommPacket readPacket(CodedInputByteBufferNano inputBuffer)
+            throws IOException {
+        CommPacket packet = Bluetooth.CommPacket.parseFrom(inputBuffer);
+        return packet;
+    }
+
+    @Override
+    public void close() {
+        try {
+            mSocket.close();
+        } catch (IOException ioe) {
+            ProvisionCommLogger.logi(ioe);
+        }
+    }
+
+    /**
+     * Determine if the socket connection held by this instance is connected.
+     * @return {@code true} if this socket is connected.
+     */
+    public boolean isConnected() {
+        return mSocket.isConnected();
+    }
+
+    /**
+     * Flushes the contents of the buffer.  For unbuffered channels, this does nothing.
+     * @throws IOException
+     */
+    public void flush() throws IOException {
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/ChannelHandler.java b/comm/src/com/android/managedprovisioning/comm/ChannelHandler.java
new file mode 100644
index 0000000..fa6c41d
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/ChannelHandler.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import com.android.managedprovisioning.comm.Bluetooth.CommPacket;
+
+import java.io.IOException;
+
+/**
+ * A Handler is a reader thread that will loop over a thread reading
+ * messages and calling a handle function.  It is designed to remove
+ * the repetitive looping nature from other classes.
+ */
+public abstract class ChannelHandler extends Thread {
+
+    private static final int MAX_IO_EXCEPTIONS = 10;
+
+    protected Channel mChannel;
+    private boolean mIsRunning;
+
+    public ChannelHandler(Channel socket) {
+        mChannel = socket;
+        mIsRunning = true;
+    }
+
+    @Override
+    public void run() {
+        int exceptionCount = 0;
+        try {
+            startConnection();
+            while (mIsRunning && mChannel.isConnected()
+                    && exceptionCount < MAX_IO_EXCEPTIONS) {
+                try {
+                    CommPacket packet = mChannel.read();
+                    handleRequest(packet);
+                } catch (IOException ioe) {
+                    ProvisionCommLogger.logd(ioe);
+                    exceptionCount++;
+                }
+            }
+        // Catch everything for graceful close.
+        } catch (Exception e) {
+            ProvisionCommLogger.loge(e);
+        } finally {
+            stopConnection();
+        }
+        if (mChannel != null) {
+            mChannel.close();
+        }
+    }
+
+    public void stopHandler() {
+        mIsRunning = false;
+    }
+
+    /**
+     * Action to take when starting connection.
+     * @throws IOException if there is an issue that should prevent the connection from starting
+     */
+    protected abstract void startConnection() throws IOException;
+
+    /**
+     * Action to take when shutting down the connection.
+     */
+    protected abstract void stopConnection();
+
+    /**
+     * Handle data sent over the communication channel.
+     * @param packet communication packet received
+     * @throws IOException if the packet could not be processed
+     */
+    protected abstract void handleRequest(CommPacket packet) throws IOException;
+
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/PacketUtil.java b/comm/src/com/android/managedprovisioning/comm/PacketUtil.java
new file mode 100644
index 0000000..5d7f18a
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/PacketUtil.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.content.Context;
+import android.os.Build;
+import android.util.DisplayMetrics;
+
+import com.android.managedprovisioning.comm.Bluetooth.StatusUpdate;
+import com.android.managedprovisioning.comm.Bluetooth.DeviceInfo;
+import com.android.managedprovisioning.comm.Bluetooth.NetworkData;
+import com.android.managedprovisioning.comm.Bluetooth.CommPacket;
+
+import java.util.Arrays;
+
+/**
+ * Handles creation of common {@code CommPacket} protos.
+ */
+public class PacketUtil {
+    /** A connection id value that signals to close the connection. */
+    public static final int END_CONNECTION = -1;
+
+    /** Sent as part of each message to indicate which device sent a message. */
+    private final String mDeviceIdentifier;
+
+    public PacketUtil(String deviceIdentifier) {
+        mDeviceIdentifier = deviceIdentifier;
+    }
+
+    /**
+     * Create a communication packet containing a status update.
+     * @param statusCode the reported provisioning state
+     * @param customData extra data sent with the status update
+     */
+    public CommPacket createStatusUpdate(int statusCode, String customData) {
+        StatusUpdate statusUpdate = new StatusUpdate();
+        statusUpdate.statusCode = statusCode;
+        statusUpdate.customData = nullSafe(customData);
+        // Create packet
+        CommPacket packet = new CommPacket();
+        packet.deviceIdentifier = mDeviceIdentifier;
+        packet.statusUpdate = statusUpdate;
+        return packet;
+    }
+
+    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
+    public CommPacket createDeviceInfo(Context context) {
+        DeviceInfo deviceInfo = new DeviceInfo();
+        deviceInfo.apiVersion = android.os.Build.VERSION.SDK_INT;
+        deviceInfo.make = nullSafe(android.os.Build.MANUFACTURER);
+        deviceInfo.model = nullSafe(android.os.Build.MODEL);
+        deviceInfo.serial = nullSafe(android.os.Build.SERIAL);
+        deviceInfo.fingerprint = nullSafe(android.os.Build.FINGERPRINT);
+        // Get memory info.
+        MemoryInfo mi = new MemoryInfo();
+        ActivityManager activityManager =
+                (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+        if (activityManager != null) {
+            activityManager.getMemoryInfo(mi);
+            deviceInfo.totalMemory = mi.totalMem;
+        }
+        // Get screen info.
+        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+        deviceInfo.screenWidthPx = metrics.widthPixels;
+        deviceInfo.screenHeightPx = metrics.heightPixels;
+        deviceInfo.screenDensity = metrics.density;
+        // Create packet
+        CommPacket packet = new CommPacket();
+        packet.deviceIdentifier = mDeviceIdentifier;
+        packet.deviceInfo = deviceInfo;
+        return packet;
+    }
+
+    public CommPacket createDataPacket(int connectionId, int status,
+            byte[] data, int len) {
+        NetworkData networkData = new NetworkData();
+        networkData.connectionId = connectionId;
+        networkData.status = status;
+        if (data != null) {
+            networkData.data =  Arrays.copyOf(data, len);
+        }
+        // Create packet
+        CommPacket packet = new CommPacket();
+        packet.deviceIdentifier = mDeviceIdentifier;
+        packet.networkData = networkData;
+        return packet;
+    }
+
+    public CommPacket createEndPacket() {
+        return createDataPacket(END_CONNECTION, NetworkData.EOF, null, 0);
+    }
+
+    public CommPacket createEndPacket(int connId) {
+        return createDataPacket(connId, NetworkData.EOF, null, 0);
+    }
+
+    private static String nullSafe(String s) {
+        return s != null ? s : "";
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/ProvisionCommLogger.java b/comm/src/com/android/managedprovisioning/comm/ProvisionCommLogger.java
new file mode 100644
index 0000000..4b52847
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/ProvisionCommLogger.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2015, The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import android.content.Context;
+import android.util.Log;
+import android.widget.Toast;
+
+/**
+ * Utility class to centralize the logging in the Provisioning app.
+ */
+public class ProvisionCommLogger {
+    private static final String TAG = "ManagedProvisioningComm";
+    private static final boolean LOG_ENABLED = true;
+
+    // Never commit this as true.
+    public static final boolean IS_DEBUG_BUILD = false;
+
+    /**
+     * Log the message at DEBUG level.
+     */
+    public static void logd(String message) {
+        if (LOG_ENABLED) {
+            Log.d(getTag(), message);
+        }
+    }
+
+    /**
+     * Log the message at DEBUG level.
+     */
+    public static void logd(String message, Throwable t) {
+        if (LOG_ENABLED) {
+            Log.d(getTag(), message, t);
+        }
+    }
+
+    /**
+     * Log the message at DEBUG level.
+     */
+    public static void logd(Throwable t) {
+        if (LOG_ENABLED) {
+            Log.d(getTag(), "", t);
+        }
+    }
+
+    /**
+     * Log the message at VERBOSE level.
+     */
+    public static void logv(String message) {
+        if (LOG_ENABLED) {
+            Log.v(getTag(), message);
+        }
+    }
+
+    /**
+     * Log the message at VERBOSE level.
+     */
+    public static void logv(String message, Throwable t) {
+        if (LOG_ENABLED) {
+            Log.v(getTag(), message, t);
+        }
+    }
+
+    /**
+     * Log the message at VERBOSE level.
+     */
+    public static void logv(Throwable t) {
+        if (LOG_ENABLED) {
+            Log.v(getTag(), "", t);
+        }
+    }
+
+    /**
+     * Log the message at INFO level.
+     */
+    public static void logi(String message) {
+        if (LOG_ENABLED) {
+            Log.i(getTag(), message);
+        }
+    }
+
+    /**
+     * Log the message at INFO level.
+     */
+    public static void logi(String message, Throwable t) {
+        if (LOG_ENABLED) {
+            Log.i(getTag(), message, t);
+        }
+    }
+
+    /**
+     * Log the message at INFO level.
+     */
+    public static void logi(Throwable t) {
+        if (LOG_ENABLED) {
+            Log.i(getTag(), "", t);
+        }
+    }
+
+    /**
+     * Log the message at WARNING level.
+     */
+    public static void logw(String message) {
+        if (LOG_ENABLED) {
+            Log.w(getTag(), message);
+        }
+    }
+
+    /**
+     * Log the message at WARNING level.
+     */
+    public static void logw(String message, Throwable t) {
+        if (LOG_ENABLED) {
+            Log.w(getTag(), message, t);
+        }
+    }
+
+    /**
+     * Log the message at WARNING level.
+     */
+    public static void logw(Throwable t) {
+        if (LOG_ENABLED) {
+            Log.w(getTag(), "", t);
+        }
+    }
+
+    /**
+     * Log the message at ERROR level.
+     */
+    public static void loge(String message) {
+        if (LOG_ENABLED) {
+            Log.e(getTag(), message);
+        }
+    }
+
+    /**
+     * Log the message at ERROR level.
+     */
+    public static void loge(String message, Throwable t) {
+        if (LOG_ENABLED) {
+            Log.e(getTag(), message, t);
+        }
+    }
+
+    /**
+     * Log the message at ERROR level.
+     */
+    public static void loge(Throwable t) {
+        if (LOG_ENABLED) {
+            Log.e(getTag(), "", t);
+        }
+    }
+
+    /**
+     * Walks the stack trace to figure out where the logging call came from.
+     */
+    static String getTag() {
+        if (IS_DEBUG_BUILD) {
+            String className = ProvisionCommLogger.class.getName();
+
+            StackTraceElement[] trace = Thread.currentThread().getStackTrace();
+            if (trace == null) {
+                return TAG;
+            }
+
+            boolean thisClassFound = false;
+            for (StackTraceElement item : trace) {
+                if (item.getClassName().equals(className)) {
+                    // we are at the current class, keep eating all items from this
+                    // class.
+                    thisClassFound = true;
+                    continue;
+                }
+
+                if (thisClassFound) {
+                    // This is the first instance of another class, which is most
+                    // likely the caller class.
+                    return TAG + String.format(
+                            "[%s(%s): %s]", item.getFileName(), item.getLineNumber(),
+                            item.getMethodName());
+                }
+            }
+        }
+        return TAG;
+    }
+
+    public static void toast(Context context, String toast) {
+        if (IS_DEBUG_BUILD) {
+            Toast.makeText(context, toast, Toast.LENGTH_LONG).show();
+        }
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/ProvisioningAcceptor.java b/comm/src/com/android/managedprovisioning/comm/ProvisioningAcceptor.java
new file mode 100644
index 0000000..5a7bed2
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/ProvisioningAcceptor.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import java.io.IOException;
+
+/**
+ * Receives status updates from remote devices.
+ */
+public interface ProvisioningAcceptor {
+    /**
+     * @return {@code true} if currently accepting connections
+     */
+    boolean isInProgress();
+
+    /**
+     * Start listening for connections from a device with the specified identifier. A device will
+     * not be allowed to connect if this method is not called with its identifier. This value is
+     * user defined and should be non-null. When a device is no longer expected to connect, or
+     * should be prevented from connecting in the future, {@link #stopListening(String)} should
+     * be called.
+     * @param deviceIdentifier expected device identifier
+     */
+    void listenForDevice(String deviceIdentifier);
+
+    /**
+     * Begin accepting connections.
+     * @throws IOException is setting up listener fails
+     */
+    void startConnection() throws IOException;
+
+    /**
+     * Stop accepting connections.
+     */
+    void stopConnection();
+
+    /**
+     * Prevent a device with the specified identifier from connecting.
+     * @param deviceIdentifier device identifier for the device that shouldn't be allowed to
+     *    connect in the future.
+     */
+    void stopListening(String deviceIdentifier);
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/ProxyConnection.java b/comm/src/com/android/managedprovisioning/comm/ProxyConnection.java
new file mode 100644
index 0000000..3a2ef3e
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/ProxyConnection.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import com.android.managedprovisioning.comm.Bluetooth.NetworkData;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The connection between a channel and a socket to a web server.
+ * It does a basic check on the first line and then passes through
+ * all data like a dummy proxy.
+ */
+public class ProxyConnection extends Thread {
+
+    private static final String CONNECT = "CONNECT";
+
+    private static final String RESPONSE_OK = "HTTP/1.1 200 OK\n\n";
+
+    private NetToBtThread mNetToBt;
+    private BtToNetThread mBtToNet;
+    private volatile boolean mNetRunning;
+
+    private Socket mNetSocket;
+    private final PipedInputStream mHttpInput;
+    private final OutputStream mHttpOutput;
+    final private int mConnId;
+    final private Channel mChannel;
+
+    private PacketUtil mPacketUtil;
+
+    public ProxyConnection(Channel channel, int connId) {
+        mChannel = channel;
+        mConnId = connId;
+        mHttpInput = new PipedInputStream();
+        mHttpOutput = new PipedOutputStream();
+        try {
+            mHttpInput.connect((PipedOutputStream) mHttpOutput);
+        } catch (IOException e) {
+            // The streams were just created so this shouldn't happen.
+            ProvisionCommLogger.loge(e);
+        }
+        mNetRunning = true;
+    }
+
+    public boolean isRunning() {
+        return mNetRunning;
+    }
+
+    public void shutdown() {
+        ProvisionCommLogger.logd("Shutting down ConnectionProcessor");
+        try {
+            mHttpOutput.close();
+        } catch (IOException io) {
+            ProvisionCommLogger.logd(io);
+        }
+        endConnection();
+    }
+
+    @Override
+    public void run() {
+        ProvisionCommLogger.logd("Creating a new socket.");
+        processConnect();
+    }
+
+    private void endConnection() {
+        try {
+            if (mChannel != null) {
+                mChannel.write(mPacketUtil.createEndPacket(mConnId));
+            } else {
+                ProvisionCommLogger.logd(
+                        "Attempted to write end of connection with null connection");
+            }
+        } catch (IOException io) {
+            ProvisionCommLogger.logd("Could not write closing packet.", io);
+        }
+        try {
+            if (mNetSocket != null) {
+                mNetSocket.close();
+            }
+        } catch (IOException io) {
+            ProvisionCommLogger.logd("Attempted to close socket when already closed.", io);
+        }
+
+        ProvisionCommLogger.logd("Ended connection");
+    }
+
+    private class NetToBtThread extends Thread {
+        @Override
+        public void run() {
+            final byte[] buffer = new byte[16384];
+
+            InputStream input = null;
+            try {
+                input = mNetSocket.getInputStream();
+                while (mNetSocket.isConnected()) {
+                    int readBytes = input.read(buffer);
+                    if (readBytes < 0) {
+                        ProvisionCommLogger.logd("Passing " + readBytes + " bytes");
+                        mChannel.write(mPacketUtil.createEndPacket(mConnId));
+                        break;
+                    }
+                    ProvisionCommLogger.logd("Passing " + readBytes + " bytes");
+                    mChannel.write(mPacketUtil.createDataPacket(mConnId, NetworkData.OK, buffer,
+                            readBytes));
+                }
+            } catch (IOException io) {
+                ProvisionCommLogger.logd("Server socket input stream is closed.");
+            } finally {
+                if (input != null) {
+                    try {
+                        input.close();
+                    } catch (IOException ex) {
+                        ProvisionCommLogger.logw(
+                                "Failed to close connection", ex);
+                    }
+                }
+            }
+            ProvisionCommLogger.logd("SocketReader is ending.");
+            mNetRunning = false;
+        }
+    }
+
+    private class BtToNetThread extends Thread {
+        @Override
+        public void run() {
+            final byte[] buffer = new byte[16384];
+            try {
+                while (true) {
+                    int readBytes = mHttpInput.read(buffer);
+                    if (readBytes < 0) {
+                        break;
+                    }
+
+                    if (mNetSocket == null) {
+                        break;
+                    } else {
+                        mNetSocket.getOutputStream().write(buffer, 0, readBytes);
+                    }
+                }
+            } catch (IOException io) {
+                ProvisionCommLogger.logd("Bluetooth input stream for this connection is closed.");
+            } finally {
+                try {
+                    mHttpInput.close();
+                } catch (IOException ex) {
+                    ProvisionCommLogger.logw("Failed to close connection", ex);
+                }
+            }
+            ProvisionCommLogger.logd("SocketWriter is ending.");
+        }
+    }
+
+    private String getLine() throws IOException {
+        ProvisionCommLogger.logi("getLine");
+        StringBuilder buffer = new StringBuilder();
+        int ch;
+        while ((ch = mHttpInput.read()) != -1) {
+            if (ch == '\r')
+                continue;
+            if (ch == '\n')
+                break;
+            buffer.append((char) ch);
+        }
+        ProvisionCommLogger.logi("Proxy reading: " + buffer);
+
+        return buffer.toString();
+    }
+
+    private void processConnect() {
+        try {
+            String requestLine = getLine()  + '\r' + '\n';
+            String[] split = requestLine.split(" ");
+
+            String method = split[0];
+            String uri = split[1];
+
+            ProvisionCommLogger.logi("Method: " + method);
+            String host = "";
+            int port = 80;
+            String toSend = "";
+
+            if (CONNECT.equals(method)) {
+                String[] hostPortSplit = uri.split(":");
+                host = hostPortSplit[0];
+                try {
+                    port = Integer.parseInt(hostPortSplit[1]);
+                } catch (NumberFormatException nfe) {
+                    port = 443;
+                }
+                uri = "Https://" + host + ":" + port;
+            } else {
+                try {
+                    URI url = new URI(uri);
+                    host = url.getHost();
+                    port = url.getPort();
+                    if (port < 0) {
+                        port = 80;
+                    }
+                } catch (URISyntaxException e) {
+                    ProvisionCommLogger.logw("Trying to proxy invalid URL", e);
+                    mNetRunning = false;
+                    return;
+                }
+                toSend = requestLine;
+            }
+
+            List<Proxy> list = new ArrayList<Proxy>();
+            try {
+                list = ProxySelector.getDefault().select(new URI(uri));
+            } catch (URISyntaxException e) {
+                ProvisionCommLogger.loge("Unable to parse URI from request", e);
+            }
+            for (Proxy proxy : list) {
+                try {
+                    if (proxy.equals(Proxy.NO_PROXY)) {
+                        mNetSocket = new Socket(host, port);
+                        if (CONNECT.equals(method)) {
+                            handleConnect();
+                        } else {
+                            toSend = requestLine;
+                        }
+                    } else {
+                        if (proxy.address() instanceof InetSocketAddress) {
+                            // Only Inets created by PacProxySelector and ProxySelectorImpl.
+                            InetSocketAddress inetSocketAddress =
+                                    (InetSocketAddress)proxy.address();
+                            // A proxy specified with an IP addr should only ever use that IP. This
+                            // will ensure that the proxy only ever connects to its specified
+                            // address. If the proxy is resolved, use the associated IP address. If
+                            // unresolved, use the specified host name.
+                            String hostName = inetSocketAddress.isUnresolved() ?
+                                    inetSocketAddress.getHostName() :
+                                    inetSocketAddress.getAddress().getHostAddress();
+                            mNetSocket = new Socket(hostName, inetSocketAddress.getPort());
+                            toSend = requestLine;
+                        } else {
+                            ProvisionCommLogger.logw("Unsupported Inet Type from ProxySelector");
+                            continue;
+                        }
+                    }
+                } catch (IOException ioe) {
+
+                }
+                if (mNetSocket != null) {
+                    break;
+                }
+            }
+            if (mNetSocket == null) {
+                mNetSocket = new Socket(host, port);
+                if (CONNECT.equals(method)) {
+                    handleConnect();
+                } else {
+                    toSend = requestLine;
+                }
+            }
+
+            // For HTTP or PROXY, send the request back out.
+            mNetSocket.getOutputStream().write(toSend.getBytes());
+
+            mNetToBt = new NetToBtThread();
+            mNetToBt.start();
+            mBtToNet = new BtToNetThread();
+            mBtToNet.start();
+        } catch (Exception e) {
+            ProvisionCommLogger.logd(e);
+            mNetRunning = false;
+        }
+    }
+
+    public void closePipe() {
+        try {
+            mHttpInput.close();
+        } catch (IOException e) {
+            ProvisionCommLogger.logd(e);
+        }
+        try {
+            mHttpOutput.close();
+        } catch (IOException e) {
+            ProvisionCommLogger.logd(e);
+        }
+    }
+
+    private void handleConnect() throws IOException {
+        while (getLine().length() != 0);
+        // No proxy to respond so we must.
+        mChannel.write(mPacketUtil.createDataPacket(mConnId, NetworkData.OK,
+                RESPONSE_OK.getBytes(),
+                RESPONSE_OK.length()));
+    }
+
+    public OutputStream getOutput() {
+        return mHttpOutput;
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/ProxyConnectionHandler.java b/comm/src/com/android/managedprovisioning/comm/ProxyConnectionHandler.java
new file mode 100644
index 0000000..c4fbe22
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/ProxyConnectionHandler.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+
+import android.os.Handler;
+
+import com.android.managedprovisioning.comm.Bluetooth.CommPacket;
+import com.android.managedprovisioning.comm.Bluetooth.DeviceInfo;
+import com.android.managedprovisioning.comm.Bluetooth.NetworkData;
+import com.android.managedprovisioning.comm.Bluetooth.StatusUpdate;
+
+import java.io.IOException;
+import java.util.Hashtable;
+import java.util.Set;
+
+/**
+ * Handles all input from a single Channel, which may be receiving packets from multiple proxy
+ * connections on the student-side tablet. Distinct proxy connections are identified by connection
+ * IDs; each connection is processed by its own Connection thread, which passes packets along to the
+ * appropriate server, and sends server responses back over Bluetooth to the student-side proxy
+ * connection.  This is a component of the Bluetooth-mediated proxy server system.
+ */
+public class ProxyConnectionHandler extends ChannelHandler {
+    private final Hashtable<Integer, ProxyConnection> mConnectionTable;
+
+    /**
+     * Set of device identifiers that are expected to connect. Packets without expected device
+     * identifiers will be ignored and their connection attempts rejected.
+     */
+    private final Set<String> mExpectedConnections;
+
+    private final StatusCallback mCallback;
+    private final Handler mCallbackHandler;
+
+    public ProxyConnectionHandler(Channel channel, Handler handler, StatusCallback callback,
+            Set<String> expectedConnections) {
+        super(channel);
+        if (callback == null) {
+            callback = new StatusCallback();
+        }
+        mCallback = callback;
+        mCallbackHandler = handler;
+        mConnectionTable = new Hashtable<Integer, ProxyConnection>();
+        mExpectedConnections = expectedConnections;
+    }
+
+    private void endConnection() throws IOException {
+        ProvisionCommLogger.logd("Ending bluetooth connection.");
+        // Acknowledge EOC received by returning message. This writes a packet without a device Id
+        mChannel.write(new PacketUtil("").createEndPacket());
+        mChannel.close();
+    }
+
+    @Override
+    protected void startConnection() throws IOException {
+
+    }
+
+    @Override
+    protected void stopConnection() {
+        try {
+            for (ProxyConnection connection : mConnectionTable.values()) {
+                connection.shutdown();
+            }
+        } catch (Exception e) {
+            ProvisionCommLogger.logd("Problem cleaning up connection", e);
+        }
+    }
+
+    @Override
+    protected void handleRequest(CommPacket packet) throws IOException {
+        // Make sure device identifier is expected
+        String deviceIdentifier = packet.deviceIdentifier;
+        if (deviceIdentifier == null || !mExpectedConnections.contains(deviceIdentifier)) {
+            ProvisionCommLogger.logd("Unexpected device: " + deviceIdentifier);
+            endConnection();
+            return;
+        }
+        // Process packet. Make sure only a single extra packet type is specified.
+        if (packet.deviceInfo != null) {
+            if (packet.networkData != null || packet.statusUpdate != null) {
+                ProvisionCommLogger.logd("Device " + deviceIdentifier + " set multiple packets.");
+                endConnection();
+                return;
+            }
+            handleDeviceInfoPacket(deviceIdentifier, packet.deviceInfo);
+        } else if (packet.networkData != null) {
+            if (packet.statusUpdate != null) {
+                ProvisionCommLogger.logd("Device " + deviceIdentifier + " set multiple packets.");
+                endConnection();
+                return;
+            }
+            handleNetworkDataPacket(packet.networkData);
+        } else if (packet.statusUpdate != null) {
+            handleStatusUpdatePacket(deviceIdentifier, packet.statusUpdate);
+        }
+    }
+
+    private void handleDeviceInfoPacket(final String deviceIdentifier,
+            final DeviceInfo deviceInfo) {
+        mCallbackHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mCallback.onDeviceCheckin(deviceIdentifier, deviceInfo);
+                } catch (Throwable t) {
+                    ProvisionCommLogger.logd("Error from callback.", t);
+                }
+            }
+        });
+    }
+
+    private void handleNetworkDataPacket(NetworkData networkData) throws IOException {
+        if (networkData.connectionId == PacketUtil.END_CONNECTION) {
+            endConnection();
+            return;
+        }
+        ProxyConnection connection = mConnectionTable.get(networkData.connectionId);
+        if (connection == null) {
+            ProvisionCommLogger.logd("Adding a stream for connection #" + networkData.connectionId);
+            connection = new ProxyConnection(mChannel, networkData.connectionId);
+            mConnectionTable.put(networkData.connectionId, connection);
+            connection.start();
+        }
+        if (networkData.status == NetworkData.EOF) {
+            ProvisionCommLogger.logd("Read EOF for conn #" + networkData.connectionId);
+            connection.shutdown();
+        } else {
+            ProvisionCommLogger.logd("Queueing " + networkData.data.length + " bytes");
+            connection.getOutput().write(networkData.data);
+            connection.getOutput().flush();
+        }
+    }
+
+    private void handleStatusUpdatePacket(final String deviceIdentifier,
+            final StatusUpdate statusUpdate) {
+        mCallbackHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mCallback.onStatusUpdate(deviceIdentifier, statusUpdate);
+                } catch (Throwable t) {
+                    ProvisionCommLogger.logd("Error from callback.", t);
+                }
+            }
+        });
+    }
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/ServerSocketWrapper.java b/comm/src/com/android/managedprovisioning/comm/ServerSocketWrapper.java
new file mode 100644
index 0000000..ced2aac
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/ServerSocketWrapper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import java.io.IOException;
+
+/**
+ * Provides an abstraction layer that wraps a BluetoothServerSocket.
+ */
+public interface ServerSocketWrapper {
+
+    /**
+     * Restart the underlying connection.
+     * @throws IOException if the connection could not be reestablished.
+     */
+    void recreate() throws IOException;
+
+    /**
+     * Listen for a Bluetooth connection. This method will block until connected.
+     * @return the connection
+     * @throws IOException if there was an error while connecting.
+     */
+    SocketWrapper accept() throws IOException;
+
+    /**
+     * Stop listening for incoming connections.
+     * @throws IOException if there was an error while closing the connection.
+     */
+    void close() throws IOException;
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/SocketWrapper.java b/comm/src/com/android/managedprovisioning/comm/SocketWrapper.java
new file mode 100644
index 0000000..051a892
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/SocketWrapper.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * SocketWrapper is an abstraction layer around a Bluetooth socket.
+ */
+public interface SocketWrapper {
+    InputStream getInputStream() throws IOException;
+    OutputStream getOutputStream() throws IOException;
+    void open() throws IOException;
+    void close() throws IOException;
+    boolean isConnected();
+    void recreate() throws IOException;
+    String getIdentifier();
+}
diff --git a/comm/src/com/android/managedprovisioning/comm/StatusCallback.java b/comm/src/com/android/managedprovisioning/comm/StatusCallback.java
new file mode 100644
index 0000000..3574a3f
--- /dev/null
+++ b/comm/src/com/android/managedprovisioning/comm/StatusCallback.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.comm;
+
+import com.android.managedprovisioning.comm.Bluetooth.DeviceInfo;
+import com.android.managedprovisioning.comm.Bluetooth.StatusUpdate;
+
+/**
+ * Receives information about connected devices.
+ *
+ * <p>Implementations can override {@link #onDeviceCheckin(String, DeviceInfo)} to receive a
+ * call when a device connects for the first time. This {@code DeviceInfo} object will provide
+ * information about the connected device.
+ *
+ * <p>Implementations can override {@link #onStatusUpdate(String, StatusUpdate)} to receive a
+ * call when a device reports its status. The {@code StatusUpdate} object will contain the
+ * current status of the device and possibly associated data.
+ */
+public class StatusCallback {
+    /**
+     * Override to receive device info when a device connects.
+     * @param deviceIdentifier uniquely identifies a device
+     * @param deviceInfo device info packet received from the remote device
+     */
+    public void onDeviceCheckin(String deviceIdentifier, DeviceInfo deviceInfo) {}
+
+    /**
+     * Override to receive device status.
+     * @param deviceIdentifier uniquely identifies a device
+     * @param statusUpdate status update packet received from the remote device
+     */
+    public void onStatusUpdate(String deviceIdentifier, StatusUpdate statusUpdate) {}
+}
diff --git a/src/com/android/managedprovisioning/proxy/BluetoothTetherClient.java b/src/com/android/managedprovisioning/proxy/BluetoothTetherClient.java
new file mode 100644
index 0000000..0a45038
--- /dev/null
+++ b/src/com/android/managedprovisioning/proxy/BluetoothTetherClient.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2015, The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.proxy;
+
+import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
+
+import com.android.managedprovisioning.ProvisionLogger;
+import com.android.managedprovisioning.comm.BluetoothSocketWrapper;
+import com.android.managedprovisioning.comm.PacketUtil;
+
+import java.io.IOException;
+
+/**
+ * Used to setup a communication link with the device that started device provisioning via NFC.
+ */
+public class BluetoothTetherClient implements ClientTetherConnection {
+    private final ReliableChannel mChannel;
+    private final PacketUtil mPacketUtil;
+    private final TetherProxy mTetherProxy;
+
+    public BluetoothTetherClient(Context context, BluetoothAdapter bluetoothAdapter,
+            String deviceIdentifier, String bluetoothMac, String bluetoothUuid) {
+        // Create communication channel
+        mPacketUtil = new PacketUtil(deviceIdentifier);
+        BluetoothSocketWrapper socket = new BluetoothSocketWrapper(bluetoothAdapter, bluetoothMac,
+                bluetoothUuid);
+        mChannel = new ReliableChannel(socket, mPacketUtil.createDeviceInfo(context),
+                mPacketUtil.createEndPacket());
+        mTetherProxy = new TetherProxy(context, mChannel, mPacketUtil);
+    }
+
+    @Override
+    public boolean sendStatusUpdate(int statusCode, String data) {
+        try {
+            mChannel.write(mPacketUtil.createStatusUpdate(statusCode, data));
+        } catch (IOException e) {
+            ProvisionLogger.loge("Failed to write status.", e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void startGlobalProxy() throws IOException {
+        // Start the connection and keep it open. This connection will be used by the proxy.
+        mChannel.createConnection();
+        mTetherProxy.startServer();
+    }
+
+    /**
+     * Calls this client's proxy and clear the global proxy.
+     */
+    public void clearGlobalProxy() {
+        mTetherProxy.clearProxy();
+    }
+
+    @Override
+    public void removeGlobalProxy() {
+        ProvisionLogger.logd("Stopping proxy");
+        mTetherProxy.stopServer();
+        // This will close the bluetooth connection.  However it will reconnect when needed.
+        mChannel.close();
+        // Wait for the proxy to stop before returning.
+        try {
+            mTetherProxy.join(1000);
+        } catch (InterruptedException e) {}
+    }
+}
diff --git a/src/com/android/managedprovisioning/proxy/ChannelInputDispatcher.java b/src/com/android/managedprovisioning/proxy/ChannelInputDispatcher.java
new file mode 100644
index 0000000..9f2be18
--- /dev/null
+++ b/src/com/android/managedprovisioning/proxy/ChannelInputDispatcher.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2015, The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.proxy;
+
+import android.util.ArrayMap;
+
+import com.android.managedprovisioning.comm.Bluetooth;
+import com.android.managedprovisioning.comm.Channel;
+import com.android.managedprovisioning.comm.ChannelHandler;
+import com.android.managedprovisioning.comm.PacketUtil;
+import com.android.managedprovisioning.comm.ProvisionCommLogger;
+import com.android.managedprovisioning.comm.Bluetooth.CommPacket;
+import com.android.managedprovisioning.comm.Bluetooth.NetworkData;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.SocketException;
+
+/**
+ * Reads network request data sent over a {@link Channel}. This implementation will can only
+ * process packets that include {@code NetworkData}. Network data received over Bluetooth will
+ * be written to the {@link OutputStream} corresponding to the data's connection Id.
+ */
+public class ChannelInputDispatcher extends ChannelHandler {
+    /** Map from connection Ids to corresponding output stream. */
+    private ArrayMap<Integer, OutputStream> mOutputStreamTable;
+
+    ChannelInputDispatcher(Channel channel) {
+        super(channel);
+        mOutputStreamTable = new ArrayMap<Integer, OutputStream>();
+    }
+
+    synchronized void addStream(int connectionId, OutputStream output) {
+        mOutputStreamTable.put(connectionId, output);
+    }
+
+    synchronized void removeStream(int connectionId) {
+        mOutputStreamTable.remove(connectionId);
+    }
+
+    synchronized boolean containsKey(int connectionId) {
+        return mOutputStreamTable.containsKey(connectionId);
+    }
+
+    synchronized OutputStream getStream(int connectionId) {
+        return mOutputStreamTable.get(connectionId);
+    }
+
+    @Override
+    protected void startConnection() throws IOException {
+
+    }
+
+    @Override
+    protected void stopConnection() {
+
+    }
+
+    /**
+     * Read network data from Bluetooth and write that data to the corresponding connection output
+     * stream.
+     * @param packet {@inheritDoc}
+     */
+    @Override
+    protected void handleRequest(CommPacket packet) throws IOException {
+        NetworkData networkData = packet.networkData;
+        if (networkData == null) {
+            ProvisionCommLogger.loge("Received packet without network data.");
+            return;
+        }
+
+        int connectionId = networkData.connectionId;
+
+        if (connectionId == PacketUtil.END_CONNECTION) {
+            ProvisionCommLogger.logw(
+                    "END_CONNECTION read from Bluetooth. Shutting down dispatcher");
+            stopHandler();
+            // Keep the channel around for status updates.
+            mChannel = null;
+            return;
+        }
+
+        if (!containsKey(connectionId)) {
+            ProvisionCommLogger.logw("No stream found for connection #" + connectionId + " of type "
+                    + networkData.status);
+            return;
+        }
+        OutputStream output = getStream(connectionId);
+        if(networkData.status == Bluetooth.NetworkData.EOF) {
+            try {
+                output.close();
+            } catch (IOException ex) {
+                ProvisionCommLogger.logw(ex);
+            }
+            removeStream(connectionId);
+            return;
+        }
+        // Write network data to output stream
+        byte[] data = networkData.data;
+        try {
+            output.write(data);
+            output.flush();
+        } catch (SocketException e) {
+            ProvisionCommLogger.logd(e);
+            removeStream(connectionId);
+        }
+    }
+
+    /**
+     * Removes records of all connections.
+     */
+    public void clearConnections() {
+        mOutputStreamTable.clear();
+    }
+}
diff --git a/src/com/android/managedprovisioning/proxy/ClientTetherConnection.java b/src/com/android/managedprovisioning/proxy/ClientTetherConnection.java
new file mode 100644
index 0000000..8846f31
--- /dev/null
+++ b/src/com/android/managedprovisioning/proxy/ClientTetherConnection.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2015, The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.proxy;
+
+import java.io.IOException;
+
+/**
+ * Interfaced used by provisioned devices to interact with remote Bluetooth connection.
+ */
+public interface ClientTetherConnection {
+    /**
+     * Start the global proxy. Network traffic will be sent over the Bluetooth connection.
+     * @throws IOException if the global proxy could not be set
+     */
+    void startGlobalProxy() throws IOException;
+
+    /**
+     * Stop sending network data over the Bluetooth connection.
+     */
+    void removeGlobalProxy();
+
+    /**
+     * Send a status update to the remote device.
+     * @param statusCode event or status type
+     * @param data
+     * @return {@code true} if the update succeeded
+     */
+    boolean sendStatusUpdate(int statusCode, String data);
+}
diff --git a/src/com/android/managedprovisioning/proxy/ReliableChannel.java b/src/com/android/managedprovisioning/proxy/ReliableChannel.java
new file mode 100644
index 0000000..5351803
--- /dev/null
+++ b/src/com/android/managedprovisioning/proxy/ReliableChannel.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2015, The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.proxy;
+
+import java.io.IOException;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.android.managedprovisioning.ProvisionLogger;
+import com.android.managedprovisioning.comm.Bluetooth.CommPacket;
+import com.android.managedprovisioning.comm.Channel;
+import com.android.managedprovisioning.comm.SocketWrapper;
+
+/**
+ * A {@link Channel} implementation that queues data and attempts to reconnect when IO errors occur.
+ * This implementation wraps an ordinary {@code Channel} that will be opened when an active
+ * connection is needed.
+ *
+ * <p>This channel starts out in a "shutdown" state. In this state, a connection will only be
+ * attempted if there is data to be written and {@link #flush()} is called. The channel may be
+ * placed in a "connected" state with {@link #createConnection()} and "shutdown" with {@link #close()}
+ *
+ * <p>When a {{@link #write(CommPacket)} is attempted, the data packet will be queued and written
+ * out as soon as the connection is available.
+ *
+ * <p>When a connection fails, it may be restarted and requests will be made again.
+ */
+public class ReliableChannel extends Channel {
+
+    /** Number of consecutive times to retry Bluetooth connection. */
+    private static final int MAX_RETRIES = 8;
+
+    /**
+     * The amount of time to keep a socket open before closing it. This gives the programmer time
+     * to process the payload before it starts getting IOExceptions.
+     */
+    private static final long CLOSING_DELAY = 5000;
+
+    private boolean mReconnectNeeded = false;
+    private final AtomicBoolean mIsShutdown;
+    private final CommPacket mAnnouncePacket;
+    private final CommPacket mEndPacket;
+
+    /** Used to synchronize reconnecting the socket. */
+    private final Object mReconnectLock = new Object();
+
+    /** Message queue. Messages to send are added by caller and removed when they can be sent. */
+    private final BlockingQueue<CommPacket> mBuffer;
+
+    /** Handles all tasks which send packets. */
+    private final ExecutorService mWriteExecutor = Executors.newSingleThreadExecutor();
+
+    public ReliableChannel(SocketWrapper socket, CommPacket announcePacket,
+            CommPacket endPacket) {
+        super(socket);
+        mAnnouncePacket = announcePacket;
+        mEndPacket = endPacket;
+        // Start off in "Shutdown" state until createConnection() is called.
+        mIsShutdown = new AtomicBoolean(true);
+        mBuffer = new LinkedBlockingQueue<>();
+    }
+
+    public void createConnection() throws IOException {
+        // Set mIsShutdown to false. If connecting fails, mIsShutdown will be set to true
+        // in retrySetupConnection().
+        mIsShutdown.set(false);
+        try {
+            mSocket.recreate();
+            onConnected();
+        } catch (IOException e) {
+            ProvisionLogger.logd(e);
+            retrySetupConnection(e);
+        }
+    }
+
+    private void retrySetupConnection(Throwable retryCause) throws IOException {
+        mReconnectNeeded = true;
+        synchronized (mReconnectLock) {
+            retrySetupConnectionLocked(retryCause);
+        }
+        onConnected();
+    }
+
+    private void onConnected() throws IOException {
+        // This is intentionally putting the announce packet at the end of the buffer.
+        // This will cause all of our queued packets to be flushed before the programmer
+        // denies us a persistent connection due to our device id.
+        if (mAnnouncePacket != null) {
+            write(mAnnouncePacket);
+        }
+        ProvisionLogger.logd("Sending device info...");
+    }
+
+    /**
+     * Try to disconnect and reconnect the backing {@code Channel}.
+     *
+     * <p>Do not call this directly. Call {@link #retrySetupConnection(Throwable)} instead.
+     * @param retryCause exception that caused this reconnect
+     * @throws IOException if the reconnect failed
+     */
+    private void retrySetupConnectionLocked(Throwable retryCause) throws IOException {
+        if (!mReconnectNeeded) return;
+        boolean c = false;
+        for (int retries=0; !c && retries < MAX_RETRIES; ++retries) {
+            super.close();
+            try {
+                Thread.sleep(computeRetryTime(retries));
+            } catch (InterruptedException e) {
+            }
+            try {
+                mSocket.recreate();
+                c = true;
+            } catch (IOException e) {
+                ProvisionLogger.logd(e);
+                retryCause = e;
+            }
+        }
+        if (!c) {
+            throw new IOException(retryCause);
+        }
+        mReconnectNeeded = false;
+    }
+
+    /**
+     * Returns the amount of time in milliseconds to wait before trying to reconnect. This time
+     * is calculated based on the number of retry attempts that have been performed.
+     * @param retries the number of times a reconnection has been retried
+     * @return the number of milliseconds to wait before reconnecting.
+     */
+    private int computeRetryTime(int retries) {
+        // Default increasing backoff, 1, 2, 4, 8, 16, 32, 64, 128.
+        // Totaling a little over 4 mins of retries.
+        return (int) Math.pow(2, retries - 1);
+    }
+
+    /**
+     * Schedule a packet to be written. The packet will be written to a queue and will be written
+     * when the backing {@code Channel} connection is open. This packet will be written immediately
+     * if the {@code Channel} is open.
+     */
+    @Override
+    public void write(CommPacket packet) throws IOException {
+        mBuffer.add(packet);
+        if (isConnected()) {
+            flush();
+        }
+    }
+
+    /**
+     * Writes all queued packets. The write will happen on a background thread.
+     */
+    @Override
+    public void flush() throws IOException {
+        mWriteExecutor.execute(new FlushBufferTask());
+    }
+
+    /**
+     * Write a packet to the {@link Channel} backing this instance.
+     * @param packet data to write
+     * @throws IOException if the write failed
+     */
+    private void unbufferedWrite(CommPacket packet) throws IOException {
+        synchronized (mWriteLock) {
+            try {
+                super.write(packet);
+            } catch (Exception e) {
+                ProvisionLogger.logd(e);
+                retrySetupConnection(e);
+                write(packet);
+            }
+        }
+    }
+
+    @Override
+    public synchronized CommPacket read() throws IOException {
+        try {
+            return super.read();
+        } catch (IOException e) {
+            ProvisionLogger.logd(e);
+            retrySetupConnection(e);
+            return read();
+        }
+    }
+
+    /**
+     * Close the backing {@code Channel} and set the shutdown state.
+     */
+    @Override
+    public void close() {
+        ProvisionLogger.logd("Closing reliable channel");
+        mIsShutdown.set(true);
+        if (mBuffer.isEmpty()) {
+            super.close();
+        }
+    }
+
+    /**
+     * Overridden to check if this {@code Channel} is in a shutdown state. The {@code Channel}
+     * backing this instance may be connected if this channel is shut down.
+     * @return {@code true} if this {@code Channel} is in the shutdown state
+     */
+    @Override
+    public boolean isConnected() {
+        return !mIsShutdown.get();
+    }
+
+    /**
+     * Task that runs on a background thread and writes all queued packets.
+     */
+    private final class FlushBufferTask implements Runnable {
+        @Override
+        public void run() {
+            try {
+                if (mBuffer.isEmpty()) {
+                    return;
+                }
+                if (mIsShutdown.get()) {
+                    ProvisionLogger.logd("Reopening connection");
+                    createConnection();
+                }
+                CommPacket message;
+                while ((message = mBuffer.poll()) != null) {
+                    unbufferedWrite(message);
+                }
+                if (mIsShutdown.get()) {
+                    unbufferedWrite(mEndPacket);
+                    try {
+                        Thread.sleep(CLOSING_DELAY);
+                    } catch (InterruptedException e) {
+                        ProvisionLogger.loge(e);
+                    }
+                }
+            } catch (IOException ioe) {
+                ProvisionLogger.loge("Failed to write all packets.", ioe);
+            } catch (Throwable t) {
+                ProvisionLogger.loge("Unexpected throwable.", t);
+            } finally {
+                if (mIsShutdown.get()) {
+                    close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Determine if the socket connection underlying this channel is connected.
+     * @return {@code true} if this socket is connected.
+     */
+    protected boolean isSocketConnected() {
+        return super.isConnected();
+    }
+}
diff --git a/src/com/android/managedprovisioning/proxy/TetherProxy.java b/src/com/android/managedprovisioning/proxy/TetherProxy.java
new file mode 100644
index 0000000..08478e7
--- /dev/null
+++ b/src/com/android/managedprovisioning/proxy/TetherProxy.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.android.managedprovisioning.proxy;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ProxyInfo;
+
+import com.android.managedprovisioning.ProvisionLogger;
+import com.android.managedprovisioning.comm.Bluetooth.NetworkData;
+import com.android.managedprovisioning.comm.Channel;
+import com.android.managedprovisioning.comm.PacketUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Sets up a connection between a server socket and a Channel connection. The output from the server
+ * socket is written to the channel, and output from the channel is written back to the socket.
+ * This allows for web access without an active web connection. All that is needed is a channel
+ * connection to a device with a web connection.
+ */
+public class TetherProxy extends Thread {
+
+    private static final String LOCALHOST = "localhost";
+    private static final int SERVER_PORT = 0;
+    private String mLocalPort = "8080";
+
+    private boolean mRunning = false;
+
+    private Context mContext = null;
+
+    private ServerSocket mServerSocket;
+    private final ReliableChannel mChannel;
+    private int mConnectionIndex;
+    private ChannelInputDispatcher mDispatcher;
+    private List<Socket> mSockets;
+
+    private final PacketUtil mPacketUtil;
+
+    public TetherProxy(Context context, ReliableChannel channel, PacketUtil packetUtil) {
+        mContext = context;
+        mChannel = channel;
+        mPacketUtil = packetUtil;
+        mSockets = new ArrayList<>();
+    }
+
+    /**
+     * Removes the global proxy from the device. This prevents new connections from using the
+     * proxy but does not close existing connections through the proxy.
+     */
+    public void clearProxy() {
+        if (isProxySet()) {
+            try {
+                ConnectivityManager cm = (ConnectivityManager)
+                        mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+                cm.setGlobalProxy(null);
+                ProvisionLogger.logd("Global proxy removed.");
+            } catch (Exception e) {
+                ProvisionLogger.loge("Problem setting proxy", e);
+            }
+        }
+    }
+
+    private boolean isProxySet() {
+        return LOCALHOST.equals(System.getProperty("http.proxyHost"))
+                && mLocalPort.equals(System.getProperty("http.proxyPort"));
+    }
+
+    private void setProxy(String host, int port) {
+        try {
+            ProxyInfo p = ProxyInfo.buildDirectProxy(host, port);
+            ConnectivityManager cm =
+                    (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+            cm.setGlobalProxy(p);
+        } catch (Exception e) {
+            ProvisionLogger.loge("Problem setting proxy", e);
+        }
+    }
+
+    /**
+     * Starts the proxy server and returns the port it is hosted on.
+     */
+    public void startServer() throws IOException {
+        mRunning = false;
+        ProvisionLogger.logd("Running bluetooth web server");
+
+        mServerSocket = new ServerSocket(SERVER_PORT);
+        mServerSocket.setReuseAddress(true);
+
+        int port = mServerSocket.getLocalPort();
+        mLocalPort = Integer.toString(port);
+        setProxy(LOCALHOST, port);
+        mDispatcher = new ChannelInputDispatcher(mChannel);
+        mDispatcher.start();
+        mConnectionIndex = 0;
+        start();
+    }
+
+    public boolean isShutdown() {
+        return (mChannel.isSocketConnected() && mServerSocket.isClosed());
+    }
+
+    // Close Bluetooth connection, close server.
+    public synchronized void stopServer() {
+        ProvisionLogger.logd("Stopping BluetoothServer");
+
+        mRunning = false;
+        if (mServerSocket != null) {
+            try {
+                mServerSocket.close();
+            } catch (IOException e) {
+                ProvisionLogger.logd(e);
+            }
+        }
+
+        try {
+            join();
+        } catch (InterruptedException e) {
+            ProvisionLogger.logd(e);
+        }
+    }
+
+    public void clearConnections() {
+        for (Socket s : mSockets) {
+            try {
+                s.getInputStream().close();
+            } catch (IOException e) {
+                // Don't care.
+            }
+            try {
+                s.getOutputStream().close();
+            } catch (IOException e) {
+                // Don't care.
+            }
+            try {
+                s.close();
+            } catch (IOException e) {
+                // Don't care.
+            }
+        }
+        mSockets.clear();
+        mDispatcher.clearConnections();
+    }
+
+    @Override
+    public void run() {
+        try {
+            mRunning = true;
+            while (mRunning) {
+                ProvisionLogger.logd("Server waiting to accept incoming connection...");
+                final Socket socket = mServerSocket.accept();
+
+                mDispatcher.addStream(mConnectionIndex, socket.getOutputStream());
+                BluetoothWriter socketConn = new BluetoothWriter(socket.getInputStream(),
+                        mConnectionIndex);
+                socketConn.start();
+                mSockets.add(socket);
+                mConnectionIndex++;
+            }
+
+        } catch (SocketException e) {
+            ProvisionLogger.logd(e);
+        } catch (IOException e) {
+            ProvisionLogger.logd(e);
+        } finally {
+            clearProxy();
+            try {
+                mServerSocket.close();
+            } catch (IOException ex) {
+                ProvisionLogger.logw("Could not close output.", ex);
+            }
+        }
+
+        mRunning = false;
+    }
+
+    /**
+     * Receives network requests from this device through the proxy and sends
+     * that request data
+     */
+    private class BluetoothWriter extends Thread {
+
+        private final InputStream mInput;
+        private final int mConnectionId;
+
+        public BluetoothWriter(InputStream fromSocket, int connectionId) {
+            mInput = fromSocket;
+            mConnectionId = connectionId;
+        }
+
+        @Override
+        public void run() {
+            final byte[] buffer = new byte[16384];
+
+            try {
+                while (mRunning) {
+                    int bytesRead = mInput.read(buffer);
+                    if (bytesRead < 0) {
+                        break;
+                    }
+                    mChannel.write(mPacketUtil.createDataPacket(mConnectionId,
+                            NetworkData.OK, buffer, bytesRead));
+                }
+                ProvisionLogger.logv("BluetoothWriter #" + mConnectionId
+                        + " reached end of socket input stream and is closing.");
+                mChannel.write(mPacketUtil.createDataPacket(mConnectionId,
+                        NetworkData.EOF, null, 0));
+            } catch (IOException io) {
+                ProvisionLogger.logd("BluetoothWriter #" + mConnectionId + " hit IOException, ending");
+            }
+        }
+    }
+}