/*
*******************************************************************************
* BTChip Bitcoin Hardware Wallet Java API
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
*
* 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.btchip.comm.android;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
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.nfc.Tag;
import android.util.Log;
import com.btchip.comm.BTChipTransport;
import com.btchip.comm.BTChipTransportFactory;
import com.btchip.comm.BTChipTransportFactoryCallback;
import java.util.HashMap;
import java.util.concurrent.LinkedBlockingQueue;
import nordpol.android.AndroidCard;
public class BTChipTransportAndroid implements BTChipTransportFactory {
private UsbManager usbManager;
private BTChipTransport transport;
private final LinkedBlockingQueue<Boolean> gotRights = new LinkedBlockingQueue<Boolean>(1);
private Tag detectedTag;
private byte[] aid;
private static final String LOG_TAG = "BTChipTransportAndroid";
private static final String ACTION_USB_PERMISSION = "USB_PERMISSION";
private static final byte TEST_APDU[] = { (byte)0xe0, (byte)0xc4, (byte)0x00, (byte)0x00, (byte)0x00 };
/**
* Receives broadcast when a supported USB device is attached, detached or
* when a permission to communicate to the device has been granted.
*/
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
String deviceName = usbDevice.getDeviceName();
if (ACTION_USB_PERMISSION.equals(action)) {
boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
false);
Log.d(LOG_TAG, "ACTION_USB_PERMISSION: " + permission + " Device: " + deviceName);
// sync with connect
gotRights.add(permission);
}
}
};
public BTChipTransportAndroid(Context context) {
usbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
}
@Override
public boolean isPluggedIn() {
if (transport != null) {
Log.d(LOG_TAG, "Check if transport is still valid");
try {
transport.exchange(TEST_APDU);
}
catch(Exception e) {
Log.d(LOG_TAG, "Error reported by transport");
try {
transport.close();
}
catch(Exception e1) {
}
transport = null;
detectedTag = null;
}
}
/*
if (detectedTag != null) {
Log.d(LOG_TAG, "Has an NFC tag");
try {
IsoDep card = IsoDep.get(detectedTag);
card.connect();
if (!card.isConnected()) {
Log.d(LOG_TAG, "Tag disconnected, clearing");
detectedTag = null;
}
else {
card.close();
}
}
catch(Throwable t) {
Log.d(LOG_TAG, "Failed to retrieve NFC tag", t);
detectedTag = null;
}
Log.d(LOG_TAG, "Tag still connected : " + detectedTag);
}
*/
return ((getDevice(usbManager) != null) || (detectedTag != null));
}
@Override
public BTChipTransport getTransport() {
return transport;
}
@Override
public boolean connect(final Context context, final BTChipTransportFactoryCallback callback) {
if (transport != null) {
try {
Log.d(LOG_TAG, "Closing previous transport");
transport.close();
Log.d(LOG_TAG, "Previous transport closed");
}
catch(Exception e) {
}
}
if (detectedTag != null) {
try {
Log.d(LOG_TAG, "Connect to NFC tag");
AndroidCard card = AndroidCard.get(detectedTag);
card.connect();
if (aid != null) {
byte[] apdu = new byte[aid.length + 5];
apdu[0] = (byte)0x00;
apdu[1] = (byte)0xA4;
apdu[2] = (byte)0x04;
apdu[3] = (byte)0x00;
apdu[4] = (byte)aid.length;
System.arraycopy(aid, 0, apdu, 5, aid.length);
byte[] response = card.transceive(apdu);
if ((response[response.length - 2] != (byte)0x90) || (response[response.length - 1] != (byte)0x00)) {
throw new RuntimeException("Select failed");
}
}
transport = new BTChipTransportAndroidNFC(card);
callback.onConnected(true);
return true;
}
catch(Exception e) {
Log.d(LOG_TAG, "NFC tag select failed", e);
detectedTag = null;
callback.onConnected(false);
return false;
}
}
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_USB_PERMISSION);
context.registerReceiver(mUsbReceiver, filter);
final UsbDevice device = getDevice(usbManager);
final Intent intent = new Intent(ACTION_USB_PERMISSION);
usbManager.requestPermission(device, PendingIntent.getBroadcast(context, 0, intent, 0));
// retry because of InterruptedException
while (true) {
try {
// gotRights.take blocks until the UsbManager gives us the rights via callback to the BroadcastReceiver
// this might need an user interaction
if (gotRights.take()) {
transport = open(usbManager, device);
callback.onConnected(true);
return true;
}
else {
callback.onConnected(false);
return true;
}
} catch (InterruptedException ignored) {
}
}
}
public static UsbDevice getDevice(UsbManager manager) {
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
for (UsbDevice device : deviceList.values()) {
if ((device.getVendorId() == VID || device.getVendorId() == VID2) &&
((device.getProductId() == PID_WINUSB) || (device.getProductId() == PID_HID) ||
(device.getProductId() == PID_NANOS) || (device.getProductId() == PID_BLUE) ||
(device.getProductId() == PID_HID_LEDGER) || (device.getProductId() == PID_HID_LEDGER_PROTON))) {
return device;
}
}
return null;
}
public static BTChipTransport open(UsbManager manager, UsbDevice device) {
// Must only be called once permission is granted (see http://developer.android.com/reference/android/hardware/usb/UsbManager.html)
// Important if enumerating, rather than being awaken by the intent notification
UsbInterface dongleInterface = device.getInterface(0);
UsbEndpoint in = null;
UsbEndpoint out = null;
boolean ledger;
for (int i=0; i<dongleInterface.getEndpointCount(); i++) {
UsbEndpoint tmpEndpoint = dongleInterface.getEndpoint(i);
if (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_IN) {
in = tmpEndpoint;
}
else {
out = tmpEndpoint;
}
}
UsbDeviceConnection connection = manager.openDevice(device);
connection.claimInterface(dongleInterface, true);
ledger = ((device.getProductId() == PID_HID_LEDGER) || (device.getProductId() == PID_HID_LEDGER_PROTON)
|| (device.getProductId() == PID_NANOS) || (device.getProductId() == PID_BLUE));
if (device.getProductId() == PID_WINUSB) {
return new BTChipTransportAndroidWinUSB(connection, dongleInterface, in, out, TIMEOUT);
}
else {
return new BTChipTransportAndroidHID(connection, dongleInterface, in, out, TIMEOUT, ledger);
}
}
public void setDetectedTag(Tag tag) {
Log.d(LOG_TAG, "Setting detected tag " + tag);
this.detectedTag = tag;
}
public void clearDetectedTag() {
detectedTag = null;
}
public void setAID(byte[] aid) {
this.aid = aid;
}
public static final String LOG_STRING = "BTChip";
public static boolean isLedgerWithScreen(final UsbDevice d) {
final int pId = d.getProductId();
final boolean screenDevice = pId == PID_NANOS || pId == PID_BLUE;
return screenDevice && d.getVendorId() == VID2;
}
private static final int VID = 0x2581;
private static final int VID2 = 0x2c97;
private static final int PID_WINUSB = 0x1b7c;
private static final int PID_HID = 0x2b7c;
private static final int PID_HID_LEDGER = 0x3b7c;
private static final int PID_HID_LEDGER_PROTON = 0x4b7c;
private static final int PID_NANOS = 0x0000;
private static final int PID_BLUE = 0x0001;
private static final int TIMEOUT = 20000;
}