summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk7
-rw-r--r--comm/Android.mk31
-rw-r--r--comm/protos/bluetooth.proto63
-rw-r--r--comm/src/com/android/managedprovisioning/comm/BluetoothAcceptor.java154
-rw-r--r--comm/src/com/android/managedprovisioning/comm/BluetoothServerSocketWrapper.java77
-rw-r--r--comm/src/com/android/managedprovisioning/comm/BluetoothSocketWrapper.java119
-rw-r--r--comm/src/com/android/managedprovisioning/comm/Channel.java131
-rw-r--r--comm/src/com/android/managedprovisioning/comm/ChannelHandler.java88
-rw-r--r--comm/src/com/android/managedprovisioning/comm/PacketUtil.java117
-rw-r--r--comm/src/com/android/managedprovisioning/comm/ProvisionCommLogger.java206
-rw-r--r--comm/src/com/android/managedprovisioning/comm/ProvisioningAcceptor.java57
-rw-r--r--comm/src/com/android/managedprovisioning/comm/ProxyConnection.java321
-rw-r--r--comm/src/com/android/managedprovisioning/comm/ProxyConnectionHandler.java163
-rw-r--r--comm/src/com/android/managedprovisioning/comm/ServerSocketWrapper.java44
-rw-r--r--comm/src/com/android/managedprovisioning/comm/SocketWrapper.java34
-rw-r--r--comm/src/com/android/managedprovisioning/comm/StatusCallback.java47
-rw-r--r--src/com/android/managedprovisioning/proxy/BluetoothTetherClient.java83
-rw-r--r--src/com/android/managedprovisioning/proxy/ChannelInputDispatcher.java129
-rw-r--r--src/com/android/managedprovisioning/proxy/ClientTetherConnection.java43
-rw-r--r--src/com/android/managedprovisioning/proxy/ReliableChannel.java269
-rw-r--r--src/com/android/managedprovisioning/proxy/TetherProxy.java233
21 files changed, 2415 insertions, 1 deletions
diff --git a/Android.mk b/Android.mk
index 429fc810..3645a91a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -3,12 +3,16 @@ include $(CLEAR_VARS)
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 frameworks/opt/setupwizard/navigationbar/common.mk
include $(BUILD_PACKAGE)
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/comm/Android.mk b/comm/Android.mk
new file mode 100644
index 00000000..70005b5f
--- /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 00000000..2536b88f
--- /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 00000000..b0b75f34
--- /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 00000000..234ac3ec
--- /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 00000000..eb30f081
--- /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 00000000..2d33ea2b
--- /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 00000000..fa6c41df
--- /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 00000000..5d7f18ac
--- /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 00000000..4b528477
--- /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 00000000..5a7bed23
--- /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 00000000..3a2ef3ec
--- /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 00000000..c4fbe222
--- /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 00000000..ced2aac1
--- /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 00000000..051a8920
--- /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 00000000..3574a3fa
--- /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 00000000..0a450384
--- /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 00000000..9f2be18f
--- /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 00000000..8846f31b
--- /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 00000000..5351803b
--- /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 00000000..08478e7b
--- /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");
+ }
+ }
+ }
+}