diff options
21 files changed, 2415 insertions, 1 deletions
@@ -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"); + } + } + } +} |