/* * Copyright 2013 Ytai Ben-Tsvi. All rights reserved. * * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARSHAN POURSOHI OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied. */ package ioio.lib.android.device; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbEndpoint; import android.hardware.usb.UsbInterface; import android.hardware.usb.UsbManager; import android.os.Build; import android.util.Log; import java.io.BufferedOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.util.Collection; import java.util.HashMap; import ioio.lib.api.IOIOConnection; import ioio.lib.api.exception.ConnectionLostException; import ioio.lib.impl.FixedReadBufferedInputStream; import ioio.lib.spi.IOIOConnectionBootstrap; import ioio.lib.spi.IOIOConnectionFactory; import ioio.lib.spi.NoRuntimeSupportException; import ioio.lib.util.android.ContextWrapperDependent; /** * A bootstrap class which allows connecting to a IOIO as a USB device from Android. Requires API-12 * or higher to build and run. Will fail gracefully if runtime API is smaller. * * @author Misha Seltzer * @author Nadir Izrael * @author Ytai Ben-Tsvi */ public class DeviceConnectionBootstrap extends BroadcastReceiver implements ContextWrapperDependent, IOIOConnectionBootstrap { private static final String TAG = "DeviceConnectionBootstrap"; private static final String ACTION_USB_PERMISSION = "ioio.lib.otg.action.USB_PERMISSION"; private static final int REQUEST_TYPE = 0x21; private static final int SET_CONTROL_LINE_STATE = 0x22; private enum State { CLOSED, WAIT_DEVICE_ATTACHED, DEVICE_ATTACHED, WAIT_DEVICE_PERMITTED, DEVICE_ZOMBIE, DEVICE_OPEN } private enum Permission { UNKNOWN, PENDING, GRANTED, DENIED } // State-related signals. private State state_ = State.CLOSED; private boolean shouldOpen_ = false; private boolean shouldOpenDevice_ = false; private Permission permission_ = Permission.UNKNOWN; // Android-ism. private ContextWrapper activity_; private PendingIntent pendingIntent_; // USB device stuff. private UsbManager usbManager_; private UsbDevice device_; private UsbDeviceConnection connection_; private UsbInterface controlInterface_; private UsbInterface dataInterface_; private UsbEndpoint epIn_; private UsbEndpoint epOut_; private InputStream inputStream_; private OutputStream outputStream_; public DeviceConnectionBootstrap() { if (Build.VERSION.SDK_INT < 12) { throw new NoRuntimeSupportException("OTG is not supported on this device."); } } @Override public void getFactories(Collection<IOIOConnectionFactory> result) { result.add(new IOIOConnectionFactory() { @Override public String getType() { return DeviceConnectionBootstrap.class.getCanonicalName(); } @Override public Object getExtra() { return null; } @Override public IOIOConnection createConnection() { return new Connection(); } }); } @Override public void onCreate(ContextWrapper wrapper) { Log.v(TAG, "onCreate()"); activity_ = wrapper; usbManager_ = (UsbManager) wrapper.getSystemService(Context.USB_SERVICE); registerReceiver(); } @Override public void onDestroy() { Log.v(TAG, "onDestroy()"); unregisterReceiver(); } @Override public synchronized void onReceive(Context context, Intent intent) { Log.v(TAG, "onReceive(" + intent + ")"); String action = intent.getAction(); if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); if (device.equals(device_)) { device_ = null; updateState(); } } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) { updateState(); } else if (ACTION_USB_PERMISSION.equals(action)) { permission_ = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false) ? Permission.GRANTED : Permission.DENIED; updateState(); } } @Override public synchronized void open() { Log.v(TAG, "open()"); shouldOpen_ = true; updateState(); } @Override public synchronized void close() { Log.v(TAG, "close()"); shouldOpen_ = false; updateState(); } @Override public void reopen() { open(); } private void setState(State state) { state_ = state; Log.v(TAG, "state <= " + state); } /** * Re-evaluate state. * <p/> * This is the main state machine governing the connection process with IOIO device. This is the * only place where the {@link #state_} field may be changed. External events may update the * control signals, then call {@link #updateState()} to act upon them. Those signals include: * <dl> * <dt>{@link #shouldOpen_}</dt> * <dd>Updated by the {@link #open()}/{@link #close()} methods to designate whether our overall * system should be active.</dd> * <dt>{@link #shouldOpenDevice_}</dt> * <dd>Updated by the connection methods to designate whether the client is interested in a * connection to the device to be established.</dd> * <dt>{@link #device_}</dt> * <dd>Set whenever the device is attached and cleared whenever it is detached.</dd> * <dt>{@link #permission_}</dt> * <dd>Designates whether we have permission to talk to the IOIO, or we have a pending request, * or we haven't checked yet.</dd> * </dl> * The state machine is organized such that the escalation in establishing a connection does not * skip steps, but rather goes through every step, but will keep checking whether we should stay * there within the same call. When there is nothing more to do, we set {@code done} to * {@code true} and thus exit the loop. * <p/> * The same is true for closing the connection - we will go backwards in several steps until * reaching the desired target state. */ private synchronized void updateState() { boolean done = false; while (!done) { switch (state_) { case CLOSED: if (shouldOpen_) { setState(State.WAIT_DEVICE_ATTACHED); } else { done = true; } break; case WAIT_DEVICE_ATTACHED: if (!shouldOpen_) { setState(State.CLOSED); } else if (checkAttached()) { permission_ = Permission.UNKNOWN; setState(State.DEVICE_ATTACHED); } else { done = true; } break; case DEVICE_ATTACHED: if (!shouldOpen_) { setState(State.CLOSED); } else if (shouldOpenDevice_) { setState(State.WAIT_DEVICE_PERMITTED); } else { done = true; } break; case WAIT_DEVICE_PERMITTED: if (!shouldOpen_ || !shouldOpenDevice_) { if (permission_ == Permission.PENDING) { pendingIntent_.cancel(); } setState(State.DEVICE_ATTACHED); } else if (device_ == null) { // Detached if (permission_ == Permission.PENDING) { pendingIntent_.cancel(); } setState(State.WAIT_DEVICE_ATTACHED); } else { checkPermission(); switch (permission_) { case PENDING: done = true; break; case GRANTED: if (openDevice()) { setState(State.DEVICE_OPEN); } else { setState(State.DEVICE_ZOMBIE); } break; case DENIED: setState(State.DEVICE_ZOMBIE); break; default: assert false; } } break; case DEVICE_ZOMBIE: if (device_ == null) { // Detached setState(State.WAIT_DEVICE_ATTACHED); } else if (!shouldOpen_ || !shouldOpenDevice_) { setState(State.DEVICE_ATTACHED); } else { done = true; } break; case DEVICE_OPEN: if (device_ == null) { // Detached setState(State.WAIT_DEVICE_ATTACHED); } else if (!shouldOpen_ || !shouldOpenDevice_) { closeDevice(); setState(State.DEVICE_ATTACHED); } else { done = true; } break; } } notifyAll(); } /** * Open a connection to the IOIO device. * <p/> * If the call succeeds it returns {@code true} and the caller is responsible to call * {@link #closeDevice()}. If the call fails, it returns (@code false} and no clean-up is * required from the caller. * * @precondition The device is attached and permission has been granted to connect to it. */ private boolean openDevice() { assert device_ != null; if (!processDescriptor()) return false; connection_ = usbManager_.openDevice(device_); if (connection_ != null) { if (openStreams()) { return true; } // If we got here, we failed. connection_.close(); connection_ = null; } return false; } /** * Close the connection, previously established by {@link #openDevice()}. */ private void closeDevice() { assert device_ != null; closeStreams(); connection_.close(); connection_ = null; } /** * Validate the device descriptor and extract the interface and endpoint descriptors. * * @return {@code true} if successful. */ private boolean processDescriptor() { assert device_ != null; if (device_.getInterfaceCount() != 2) { Log.e(TAG, "UsbDevice doesn't have exactly 2 interfaces."); return false; } controlInterface_ = device_.getInterface(0); dataInterface_ = device_.getInterface(1); if (controlInterface_.getEndpointCount() != 1) { Log.e(TAG, "Control interface (0) of UsbDevice doesn't have exactly 1 endpoints."); return false; } if (dataInterface_.getEndpointCount() != 2) { Log.e(TAG, "Data interface (1) of UsbDevice doesn't have exactly 2 endpoints."); return false; } final UsbEndpoint ep0 = dataInterface_.getEndpoint(0); final UsbEndpoint ep1 = dataInterface_.getEndpoint(1); if (ep0.getDirection() == UsbConstants.USB_DIR_IN && ep1.getDirection() == UsbConstants.USB_DIR_OUT) { epIn_ = ep0; epOut_ = ep1; } else if (ep0.getDirection() == UsbConstants.USB_DIR_OUT && ep1.getDirection() == UsbConstants.USB_DIR_IN) { epIn_ = ep1; epOut_ = ep0; } else { Log.e(TAG, "Endpoints directions are not compatible."); return false; } return true; } /** * Claim interfaces and create the I/O streams. * <p/> * If the call succeeds it returns {@code true} and the caller is responsible to call * {@link #closeStreams()}. If the call fails, it returns (@code false} and no clean-up is * required from the caller. * * @precondition The device is attached, permission has been granted to connect to it, and the * descriptor has been processed. */ private boolean openStreams() { // Claim interfaces. if (connection_.claimInterface(controlInterface_, true)) { if (connection_.claimInterface(dataInterface_, true)) { // Raise DTR. if (setDTR(true)) { // Create streams. Buffer them with a reasonable buffer sizes. inputStream_ = new FixedReadBufferedInputStream(new Streams.DeviceInputStream( connection_, epIn_), 1024); outputStream_ = new BufferedOutputStream(new Streams.DeviceOutputStream( connection_, epOut_), 1024); return true; } else { Log.e(TAG, "Failed to set DTR to true"); } // If we got here, we failed. connection_.releaseInterface(dataInterface_); Log.e(TAG, "Failed to claim UsbInterface 1"); } // If we got here, we failed. connection_.releaseInterface(controlInterface_); } else { Log.e(TAG, "Failed to claim UsbInterface 0"); } // If we got here, we failed. return false; } /** * Close the streams and release the interfaces, to clean-up a successful call to * {@link #openStreams()}. */ void closeStreams() { setDTR(false); connection_.releaseInterface(controlInterface_); connection_.releaseInterface(dataInterface_); } /** * Sends a DTR signal to the IOIO. * <p/> * The IOIO uses this signal to indicate whether a client is currently connected to it. * * @param {@code true} means start a session, {@code false} means end a session. * @return {@code true} if the transfer succeeded. */ private boolean setDTR(boolean dtr) { return 0 == connection_.controlTransfer(REQUEST_TYPE, SET_CONTROL_LINE_STATE, dtr ? 0x01 : 0x00, 0, null, 0, Streams.TRANSFER_TIMEOUT_MILLIS); } /** * Check whether the IOIO is attached. * <p/> * Returns fast if has previously returned positively. * * @return {@code true} If attached. In this case the {@link #device_} field will contain the * {@link UsbDevice}. */ private boolean checkAttached() { if (device_ != null) return true; HashMap<String, UsbDevice> devicesMap = usbManager_.getDeviceList(); for (UsbDevice device : devicesMap.values()) { if (device.getProductId() != 0x0008 || device.getVendorId() != 0x1B4F) { // This is not IOIO :(. continue; } device_ = device; return true; } return false; } /** * Check the status of the permission to connect to the IOIO. * <p/> * If it is yet unknown whether we have permission or not, will issue an permission request. * Updates the {@link #permission_} field. */ private void checkPermission() { if (permission_ == Permission.UNKNOWN) { if (usbManager_.hasPermission(device_)) { permission_ = Permission.GRANTED; } else { pendingIntent_ = PendingIntent.getBroadcast(activity_, 0, new Intent( ACTION_USB_PERMISSION), 0); usbManager_.requestPermission(device_, pendingIntent_); permission_ = Permission.PENDING; } } } private void registerReceiver() { IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED); activity_.registerReceiver(this, filter); } private void unregisterReceiver() { activity_.unregisterReceiver(this); } private enum InstanceState { INIT, CONNECTED, DEAD } ; /** * The actual {@link IOIOConnection} implementation. * <p/> * Doesn't do much, except signal the external state machine whether or not we're interested in * an open connection, and waits for the external state machine to reach the desired states. */ class Connection implements IOIOConnection { private InstanceState instanceState_ = InstanceState.INIT; @Override public void waitForConnect() throws ConnectionLostException { Log.v(TAG, "waitForConnect()"); synchronized (DeviceConnectionBootstrap.this) { if (instanceState_ != InstanceState.INIT) { throw new IllegalStateException("waitForConnect() may only be called once"); } shouldOpenDevice_ = true; updateState(); while (instanceState_ != InstanceState.DEAD && state_ != State.DEVICE_OPEN) { try { DeviceConnectionBootstrap.this.wait(); } catch (InterruptedException e) { Log.e(TAG, "waitForConnect() was interrupted"); } } if (instanceState_ == InstanceState.DEAD) { throw new ConnectionLostException(); } instanceState_ = InstanceState.CONNECTED; } } @Override public void disconnect() { Log.v(TAG, "disconnect()"); synchronized (DeviceConnectionBootstrap.this) { shouldOpenDevice_ = false; instanceState_ = InstanceState.DEAD; updateState(); } } @Override public boolean canClose() { return true; } @Override public InputStream getInputStream() throws ConnectionLostException { return inputStream_; } @Override public OutputStream getOutputStream() throws ConnectionLostException { return outputStream_; } } }