14.USB连接

14.1 问题

应用程序需要与USB设备进行通信来控制或传输数据。

14.2 解决方案

(API Level 12)
对于拥有USB主机电路的设备,Android以及内置了对它的支持,可以与已经连接的USB设备进行模拟和通信。USBManager是一项系统服务,可以让应用程序访问任何通过USB连接的外部设备,接下来我们将看一下在应用程序中如何使用这个服务来建立连接。
设备上的USB主机电路已经越来越普及,但还是很普及,但还是很稀少。刚开始,只有平板电脑设备拥有这种能力,但随着科技的快速发展,在商用Android手机上它也可能很快成为一个通用的接口。正因为如此,无疑需要在应用程序的清单中中包含以下元素:

 <uses-feature android:name="android.hareware.usb.host"/>

这样只有真正拥有相应硬件的设备,才可以使用你的应用程序。
Android提供的API和USB规范几乎一样,并没有更多更深入的知识。这就意味着如果想要使用这些API,你至少需要了解一些USB的基础知识以及设备间是如何通信的。

USB概述
在查看Android是如何与USB设备进行交互的示例之前,让我们花点时间定义一些USB术语。

  • 端点:USB设备的最小构件。应用程序最终就是通过连接这些端点发送和接收数据的。端点主要分为4种类型:
    控件传输:用于配置和状态命名。每台设备至少有一个控制端点,即“端点0”,它不会关联任何接口。
    中断传输:用于小量的、高优先级的控制命令。
    批量传输:用于传输大数据。通常都是双向成对出现的(1IN和1OUT)。
    同步传输:用于实时数据传输,如音频。撰写本书时,最新的Android SDK还不支持这个功能。
  • 接口:端点的集合,用来表示一台“逻辑”设备。
    多台设备USB设备对于主机来说可以呈现为多台逻辑设备,即通过暴露多个接口来标识。
  • 配置:一个或多个接口的集合。USB协议强制规定一台设备在某个特定时间只能有一个配置是激活的。事实上,多数设备也就只有一个配置,并把它作为设备的操作模式。

14.3 实现机制

以下两段清单代码演示了使用UsbManager来检查通过USB连接的设备以及使用控制传输来进一步查询配置的示例。
res/layout/main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <Button
        android:id="@+id/button_connect"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Connect"
        android:onClick="onConnectClick" />
    <TextView
        android:id="@+id/text_status"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/text_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

USB主机上查询设备的Activity

public class USBActivity extends Activity {
    private static final String TAG = "UsbHost";

    TextView mDeviceText, mDisplayText;
    Button mConnectButton;
    
    UsbManager mUsbManager;
    UsbDevice mDevice;
    PendingIntent mPermissionIntent;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mDeviceText = (TextView) findViewById(R.id.text_status);
        mDisplayText = (TextView) findViewById(R.id.text_data);
        mConnectButton = (Button) findViewById(R.id.button_connect);
        
        mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(ACTION_USB_PERMISSION), 0);
        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        registerReceiver(mUsbReceiver, filter);        
        //检查当前连接的设备
        updateDeviceList();
    }

    @Override
    protected void onPause() {
        super.onPause();
        unregisterReceiver(mUsbReceiver);
    }

    public void onConnectClick(View v) {
        if (mDevice == null) {
            return;
        }
        mDisplayText.setText("---");
        
        //这里如果用户已经授权,就会立即发送ACTION_USB_PERMISSION 
        // 否则会向用户显示授权对话框
        mUsbManager.requestPermission(mDevice, mPermissionIntent);
    }
    
    /*
     * 捕捉用户权限响应的接收器,在和已经连接的设备进行真正的交互时是需要这些权限的
     */
    private static final String ACTION_USB_PERMISSION = "com.android.recipes.USB_PERMISSION";
    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (ACTION_USB_PERMISSION.equals(action)) {
                UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);

                if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)
                        && device != null) {
                        //查询设备的描述符
                        getDeviceStatus(device);
                } else {
                    Log.d(TAG, "permission denied for device " + device);
                }
            }
        }
    };

    //类型: 表示读写还是写入
    // 与USB_ENDPOINT_DIR_MASK 进行匹配,判断IN还是OUT
    private static final int REQUEST_TYPE = 0x80;
    //请求: GET_CONFIGURATION_DESCRIPTOR = 0x06
    private static final int REQUEST = 0x06;
    //值: 描述符类型 (高) 和索引值 (低)
    // Configuration Descriptor = 0x2
    // Index = 0x0 (第一次配置)
    private static final int REQ_VALUE = 0x200;
    private static final int REQ_INDEX = 0x00;
    private static final int LENGTH = 64;
  /**
    *初始化控制传输来请求设备的第一个配置描述符
    */
    private void getDeviceStatus(UsbDevice device) {
        UsbDeviceConnection connection = mUsbManager.openDevice(device);
        //为传入的数据创建一个足够大的缓冲区
        byte[] buffer = new byte[LENGTH];
        connection.controlTransfer(REQUEST_TYPE, REQUEST, REQ_VALUE, REQ_INDEX,
                buffer, LENGTH, 2000);
        //将接收到的数据解析为描述符
        String description = parseConfigDescriptor(buffer);
        
        mDisplayText.setText(description);
        connection.close();
    }
    
    /*
     * 按照 USB 规范解析USB 配置描述符响应信息。返回可打印的连接设备的信息  
     */
    private static final int DESC_SIZE_CONFIG = 9;
    private String parseConfigDescriptor(byte[] buffer) {
        StringBuilder sb = new StringBuilder();
        //解析配置描述符的头信息
        int totalLength = (buffer[3] &0xFF) << 8;
        totalLength += (buffer[2] & 0xFF);
        //接口数量
        int numInterfaces = (buffer[5] & 0xFF);
        //配置的属性
        int attributes = (buffer[7] & 0xFF);        
        //电量递增2mA 
        int maxPower = (buffer[8] & 0xFF) * 2;
        
        sb.append("Configuration Descriptor:\n");
        sb.append("Length: " + totalLength + " bytes\n");
        sb.append(numInterfaces + " Interfaces\n");
        sb.append(String.format("Attributes:%s%s%s\n",
                (attributes & 0x80) == 0x80 ? " BusPowered" : "",
                (attributes & 0x40) == 0x40 ? " SelfPowered" : "",
                (attributes & 0x20) == 0x20 ? " RemoteWakeup" : ""));
        sb.append("Max Power: " + maxPower + "mA\n");
        
        //描述符的剩余部分为接口和端口信息
        int index = DESC_SIZE_CONFIG; 
        while (index < totalLength) {
            //读取长度和类型
            int len = (buffer[index] & 0xFF);
            int type = (buffer[index+1] & 0xFF);
            switch (type) {
            case 0x04: //接口描述符
                int intfNumber = (buffer[index+2] & 0xFF);
                int numEndpoints = (buffer[index+4] & 0xFF);
                int intfClass = (buffer[index+5] & 0xFF);
                
                sb.append(String.format("- Interface %d, %s, %d Endpoints\n",
                        intfNumber, nameForClass(intfClass), numEndpoints));
                break;
            case 0x05: //端点描述符
                int endpointAddr = ((buffer[index+2] & 0xFF));
                //端口号为 4 位
                int endpointNum = (endpointAddr & 0x0F);
                //方向为空位
                int direction = (endpointAddr & 0x80);
                
                int endpointAttrs = (buffer[index+3] & 0xFF);
                //类型为低两位
                int endpointType = (endpointAttrs & 0x3);
                
                sb.append(String.format("-- Endpoint %d, %s %s\n",
                        endpointNum,
                        nameForEndpointType(endpointType),
                        nameForDirection(direction) ));
                break;
            }
            //继续下一个描述符
            index += len;
        }
        
        return sb.toString();
    }
    
    private void updateDeviceList() {
        HashMap<String, UsbDevice> connectedDevices = mUsbManager
                .getDeviceList();
        if (connectedDevices.isEmpty()) {
            mDevice = null;
            mDeviceText.setText("No Devices Currently Connected");
            mConnectButton.setEnabled(false);
        } else {
            StringBuilder builder = new StringBuilder();
            for (UsbDevice device : connectedDevices.values()) {
                //打开最后一台 (如果有多台的话) 检测到的设备
                mDevice = device;
                builder.append(readDevice(device));
                builder.append("\n\n");
            }
            mDeviceText.setText(builder.toString());
            mConnectButton.setEnabled(true);
        }
    }

    /*
     * 遍历所有已经连接的设备的端口和接口
     * 这里不涉及权限,在尝试连接真实设备之前这些都是“公开可用”的
    */
    private String readDevice(UsbDevice device) {
        StringBuilder sb = new StringBuilder();
        sb.append("Device Name: " + device.getDeviceName() + "\n");
        sb.append(String.format(
                "Device Class: %s -> Subclass: 0x%02x -> Protocol: 0x%02x\n",
                nameForClass(device.getDeviceClass()),
                device.getDeviceSubclass(), device.getDeviceProtocol()));

        for (int i = 0; i < device.getInterfaceCount(); i++) {
            UsbInterface intf = device.getInterface(i);
            sb.append(String
                    .format("+--Interface %d Class: %s -> Subclass: 0x%02x -> Protocol: 0x%02x\n",
                            intf.getId(),
                            nameForClass(intf.getInterfaceClass()),
                            intf.getInterfaceSubclass(),
                            intf.getInterfaceProtocol()));

            for (int j = 0; j < intf.getEndpointCount(); j++) {
                UsbEndpoint endpoint = intf.getEndpoint(j);
                sb.append(String.format("  +---Endpoint %d: %s %s\n",
                        endpoint.getEndpointNumber(),
                        nameForEndpointType(endpoint.getType()),
                        nameForDirection(endpoint.getDirection())));
            }
        }

        return sb.toString();
    }

    /* 辅助方法,用来为 USB 常量提供可读性更强的名称 */
    
    private String nameForClass(int classType) {
        switch (classType) {
        case UsbConstants.USB_CLASS_APP_SPEC:
            return String.format("Application Specific 0x%02x", classType);
        case UsbConstants.USB_CLASS_AUDIO:
            return "Audio";
        case UsbConstants.USB_CLASS_CDC_DATA:
            return "CDC Control";
        case UsbConstants.USB_CLASS_COMM:
            return "Communications";
        case UsbConstants.USB_CLASS_CONTENT_SEC:
            return "Content Security";
        case UsbConstants.USB_CLASS_CSCID:
            return "Content Smart Card";
        case UsbConstants.USB_CLASS_HID:
            return "Human Interface Device";
        case UsbConstants.USB_CLASS_HUB:
            return "Hub";
        case UsbConstants.USB_CLASS_MASS_STORAGE:
            return "Mass Storage";
        case UsbConstants.USB_CLASS_MISC:
            return "Wireless Miscellaneous";
        case UsbConstants.USB_CLASS_PER_INTERFACE:
            return "(Defined Per Interface)";
        case UsbConstants.USB_CLASS_PHYSICA:
            return "Physical";
        case UsbConstants.USB_CLASS_PRINTER:
            return "Printer";
        case UsbConstants.USB_CLASS_STILL_IMAGE:
            return "Still Image";
        case UsbConstants.USB_CLASS_VENDOR_SPEC:
            return String.format("Vendor Specific 0x%02x", classType);
        case UsbConstants.USB_CLASS_VIDEO:
            return "Video";
        case UsbConstants.USB_CLASS_WIRELESS_CONTROLLER:
            return "Wireless Controller";
        default:
            return String.format("0x%02x", classType);
        }
    }

    private String nameForEndpointType(int type) {
        switch (type) {
        case UsbConstants.USB_ENDPOINT_XFER_BULK:
            return "Bulk";
        case UsbConstants.USB_ENDPOINT_XFER_CONTROL:
            return "Control";
        case UsbConstants.USB_ENDPOINT_XFER_INT:
            return "Interrupt";
        case UsbConstants.USB_ENDPOINT_XFER_ISOC:
            return "Isochronous";
        default:
            return "Unknown Type";
        }
    }

    private String nameForDirection(int direction) {
        switch (direction) {
        case UsbConstants.USB_DIR_IN:
            return "IN";
        case UsbConstants.USB_DIR_OUT:
            return "OUT";
        default:
            return "Unknown Direction";
        }
    }
}

当Activtiy首次进入前台时,它注册一个自定义动作的BroadcastReceiver,并且通过UsbManager.getDeviceList()方法来查询当前已连接舍不得列表,该方法会返回一个UsbDevice项的HashMap,然后就可以遍历和查询这个HashMap。对于每台连接的设备,我们会查询它的接口和端口,并且会构建 需要显示给用户的每台设备的描述信息。然后,我们会在用户界面上打印所有这些信息。

注意:
就目前来说,这个应用程序不需要在清单中声明任何权限。对于只是简单地查询连接到主机的设备的信息,并不需要声明权限。

如你所见,对于你想与之通信的连接设备,UsbManager提供的API可以获得你想要的所有信息。所有标准的定义,如设备种类、端点类型和传输方向也都在UsbManager中做了定义,所以不需要自己定义就可以匹配想要的类型。
那么为什么要注册BroadcastReceiver呢?在用户按下屏幕上的Connect按钮后,这个示例的剩余部分做了相应的响应。这时候我们想要与连接的设备进行真正的交互,这时候就需要用户权限。在此,当用户单击按钮时,会调用UsbManager.requestPermission()来询问用户是否可以连接。如果还没有授权相应的权限,用户会看到询问授权连接的对话框。
如果选择确认授权,传入方法的PendingIntent就会被触发。在示例中,这个Intent是通过自定义动作字符串做广播的,此时会触发BroadcastReceiver的onReceiver()方法;接下来任何的requestPermission()调用都会立即触发这个接收器。在接收器内部,我们会检查以确保结果是授权响应并通过UsbManager.openDeceive()打开与设备的连接,如果连接成功,则会返回一个UsbManagerConnection实例。
对于有效的连接,我们会通过控制传输来请求设备的配置描述符,从而得到设备更加详细的信息。控制传输一般都是通过设备的“端口0”来请求的。我们则分配一个合适大小的缓冲区来保证可以得到所有的信息。
controlTransfer()返回后,缓冲区中已经填好了响应数据。接下来应用程序会处理这些数据,得到设备的一些详细信息,例如设备的最大能耗以及设备是使用USB供电(总线供电)还是其他方式外部供电(自供电)。这个示例只是从这些标识符中解析出一小部分有用的信息。同样,所有解析出来的数据就会被放到一个字符串报告中并显示在用户界面上。
第一节中从框架API读取的信息和第二节中直接从设备读取的信息是一样的,并且按照1:1的比例通过两个文本报告显示在用户屏幕上。需要注意的一点就是,只有在设备连接上时应用程序才会工作:对于应用程序在前台运行时才连接的设备,应用程序并不会得到通知。

获取设备连接时的通知
要想在Android在设备连接时可以通知你的应用程序,需要在清单中通过<intent-filter>注册要匹配的设备类型。以下两段代码清单演示了这个过程。

AndroidManifest.xml中的部分代码

        <activity
            android:name=".USBActivity"
            android:label="@string/title_activity_usb" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>

            <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>

res/xml/device_filter.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device />
</resources>

能够处理设备连接的Activity添加一个名为USB_DEVICE_ATTACHED动作字符串的过滤器和描述想要处理设备的一些XML元数据信息。可以<usb-device>中添加很多设备属性字段,从而过滤哪些连接事件可以通知到应用程序:

  • vendor-id
  • product-id
  • class
  • subclass
  • protocol

必要时,可以定义以上很多属性来适应你的应用程序。例如,如果只想和某一台特定设备进行通信,或许可以想示例代码一样同时定义vendor-id和product-id。如果相匹配某一类型的设备(例如,所有的大数据存储设备),或许只需要定义class属性即可。甚至可以不定义任何属性,这样应用程序就可以匹配所有连接的设备。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容