Android USB转串口编程
2015-11-05 23:24
471 查看
安卓手机的对外通信接口就只有USB跟音频口,我们可采用其进行与外设进行通信。今天,我们来讲讲安卓手机利用USB接口与外设进行通信。此时,有两种情况。
第一:USB(手机)<--->USB(外设),就是手机与外设直接通过USB线进行通讯。
第二:USB(手机)<--->UART(外设),即手机与外设通过USB转接成串口,再与外设通信。
外设,说白了就是单片机,单片机端直接通过USB接口跟其他设备通讯,这种情况还是比较少见的。(另外,单片机是否具备驱动USB的能力,这个也值得质疑。我不是做下位机的,这些问题不太了解,只是说理论上这种方式是可行的而已。)一般来说,我们的单片机是通过串口跟其他设备进行通信的。所以,本文就自第二种情况进行阐述。他们的关系如下图所示。
在此,转接芯片承担着一个USB--UART转接的作用,CH34X是指具体一系列型号的芯片,包括CH340和CH341,此次我选择的是CH340,之前曾用过PL2303HA芯片作为转接芯片,但是手机端并不能很好地识别到USB设备,所以后来选择了CH340。MCU设备端的编程,是单片机工程师的串口编程,这个我们不太管,本文中我们更关注的是Android端的USB Host编程。
说到安卓USBHost编程,其实就是安卓的USB HOST编程。这部分网上资料很多,在此我大概用阐述一下。
Android下的USB Host介绍和开发
一、USB Host介绍
USB Host,中文意思是USB主模式,是相对于USB Accessory(USB副模式)来说的。如果Android工作在USB Host模式下,则连接到Android上的USB设备把Android类似的看作是一台PC机,PC机能干的事儿,Android也能干,例如将鼠标、键盘插入则可以使用键盘、鼠标来操作Android系统,如果插入U盘则,通过Android可以读写U盘上的数据。而USB
Accessory模式表示将Android设备类似当作一个USB的键盘、鼠标、U盘插入到电脑主机上一样使用,这两种模式在Android API level-12以上才支持,即Android3.1及更高的版本支持这两种模式。
在我们这里,Android端当然是主模式。
要实现安卓USB HOST编程,系统版本只是他的软件要求,除此之外,当然还需要硬件要求。那就是手机必须支持OTG功能。不知道什么是OTG的朋友,请自行百度。
二、USB HOST开发
关于Android USB HOST编程,网上这方面的例子也是不少的,大致可以实现从发现设备到打开连接设备并进行数据双向传输的功能。但这些例子只是单纯的进行USB通信,对于我们本文中的例子,是不够的。我们这里,USB设备之外是一个UART串口设备。虽然所对于我们的安卓端来说,转接芯片已经帮我们屏蔽了串口,已转接成USB,貌似已不用关心串口。但是,实际上,串口的
一些参数配置我们还是需要的。举个例子:如果MCU端以9600bps的波特率发送数据过来,转接芯片又如何知道应该以怎样的波特率进行接收并转换成USB数据呢?倘若转接芯片不是以相应的波特率接收数据,手机端必定接收到的是错误的数据。即使转接芯片默认的波特率是9600,得到了正确的数据,但假如MCU换了波特率,是以115200来发送呢?所以,我们原则上来说,手机端还是要通过一定的方法,发送一定的命令给CH340转接芯片,配置相应的串口参数。下面,我将会从如何进行USB HOST编程基础开始讲起,对这些问题进行阐述。
其实不管是进行什么样方式的通信,归纳起来,无非就是四个步骤走:发现设备----->打开(连接)设备----->数据通信----->关闭设备。
1、添加权限(安卓动不动就要添加权限,这个还是些许无聊的。)
在res目录下,新建一个xml文件夹,在里面新建一个xml文件,device_filter.xml,用来存放usb的设备信息。
新建完文件,我们自然要引用他,即在manifest.xml文件中声明。在要启动的Activity里添加 <meta-data />标签。
3、枚举(发现)并获取相应的USB设备(相关USB类:UsbManager ,UsbDevice)
不管进行什么通信,我们首先该做的应是获取相应的设备。此处,应是利用UsbManager
类获取插入到此Andriod手机上的USB设备,用UsbDevice类表示。
此处较为复杂,其实打开设备只是一个概括性的动作,实际上他还分好几个动作,不然直接打开设备的话无法通信。
打开设备方法:
获取到UsbDevice之后,我们在该设备上要获取相应的UsbInterface。
获取到USB设备接口之后,我们还不能进行USB通信,USB通信需要使用UsbEndpoint端点。
4.3
打开并连接USB设备
此处跟前面获取端点是可以互换顺序的。但从理解角度来说,我还是倾向于把打开设备放在最后,因为一般都是把配置方面都做好了,才去连接设备的。
此处直接使用UsbManager的方法进行连接:mUsbmanager.openDevice(mDevice);
到了这里,一般的即可进行USB数据的读写了。
5、读写USB数据
读数据:使用 mConn.bulkTransfer()方法,mBulkInPoint端点。另外,串口是的缓存很小的,最大也就几K,可认为几乎没有,具体大小取决于串口设备端的MCU,所以我们必须开一个线程去不断循环读取数据,不然倘若读取速度小于设备端的发送速度,这样就会导致数据丢失。
写数据:也是使用 mConn.bulkTransfer()方法,但是却用的mBulkOutPoint端点。
6、串口配置
在我们的这个CH340单转接芯片里,我们采用的是mDeviceConnection.controlTransfer()这个方法来进行配置。
6.1 :初始化串口:
6.2:配置串口参数(主要是波特率,数据位,停止位,奇偶校验位以及流控)
7、关闭设备
当然,最后不要忘记关闭设备,释放资源。
本文主要参考CH34X系列厂商提供的官方开发手册以及demo,有兴趣的朋友可以直接前往官网下载相关资料。 http://www.wch.cn/download/CH341SER_ANDROID_ZIP.html
本人知识有限,如果有说的不对的地方,欢迎留言指点,大家一起交流。
另外亦可以加Q交流,QQ:469325534
。
附源代码下载地址:http://download.csdn.net/detail/ever_gz/9250423。
第一:USB(手机)<--->USB(外设),就是手机与外设直接通过USB线进行通讯。
第二:USB(手机)<--->UART(外设),即手机与外设通过USB转接成串口,再与外设通信。
外设,说白了就是单片机,单片机端直接通过USB接口跟其他设备通讯,这种情况还是比较少见的。(另外,单片机是否具备驱动USB的能力,这个也值得质疑。我不是做下位机的,这些问题不太了解,只是说理论上这种方式是可行的而已。)一般来说,我们的单片机是通过串口跟其他设备进行通信的。所以,本文就自第二种情况进行阐述。他们的关系如下图所示。
在此,转接芯片承担着一个USB--UART转接的作用,CH34X是指具体一系列型号的芯片,包括CH340和CH341,此次我选择的是CH340,之前曾用过PL2303HA芯片作为转接芯片,但是手机端并不能很好地识别到USB设备,所以后来选择了CH340。MCU设备端的编程,是单片机工程师的串口编程,这个我们不太管,本文中我们更关注的是Android端的USB Host编程。
说到安卓USBHost编程,其实就是安卓的USB HOST编程。这部分网上资料很多,在此我大概用阐述一下。
Android下的USB Host介绍和开发
一、USB Host介绍
USB Host,中文意思是USB主模式,是相对于USB Accessory(USB副模式)来说的。如果Android工作在USB Host模式下,则连接到Android上的USB设备把Android类似的看作是一台PC机,PC机能干的事儿,Android也能干,例如将鼠标、键盘插入则可以使用键盘、鼠标来操作Android系统,如果插入U盘则,通过Android可以读写U盘上的数据。而USB
Accessory模式表示将Android设备类似当作一个USB的键盘、鼠标、U盘插入到电脑主机上一样使用,这两种模式在Android API level-12以上才支持,即Android3.1及更高的版本支持这两种模式。
在我们这里,Android端当然是主模式。
要实现安卓USB HOST编程,系统版本只是他的软件要求,除此之外,当然还需要硬件要求。那就是手机必须支持OTG功能。不知道什么是OTG的朋友,请自行百度。
二、USB HOST开发
关于Android USB HOST编程,网上这方面的例子也是不少的,大致可以实现从发现设备到打开连接设备并进行数据双向传输的功能。但这些例子只是单纯的进行USB通信,对于我们本文中的例子,是不够的。我们这里,USB设备之外是一个UART串口设备。虽然所对于我们的安卓端来说,转接芯片已经帮我们屏蔽了串口,已转接成USB,貌似已不用关心串口。但是,实际上,串口的
一些参数配置我们还是需要的。举个例子:如果MCU端以9600bps的波特率发送数据过来,转接芯片又如何知道应该以怎样的波特率进行接收并转换成USB数据呢?倘若转接芯片不是以相应的波特率接收数据,手机端必定接收到的是错误的数据。即使转接芯片默认的波特率是9600,得到了正确的数据,但假如MCU换了波特率,是以115200来发送呢?所以,我们原则上来说,手机端还是要通过一定的方法,发送一定的命令给CH340转接芯片,配置相应的串口参数。下面,我将会从如何进行USB HOST编程基础开始讲起,对这些问题进行阐述。
其实不管是进行什么样方式的通信,归纳起来,无非就是四个步骤走:发现设备----->打开(连接)设备----->数据通信----->关闭设备。
1、添加权限(安卓动不动就要添加权限,这个还是些许无聊的。)
<uses-feature android:name="android.hardware.usb.host" /> <uses-permission android:name="android.hardware.usb.host" /> <uses-permission android:name="ANDROID.PERMISSION.HARDWARE_TEST" />2、添加USB设备信息
在res目录下,新建一个xml文件夹,在里面新建一个xml文件,device_filter.xml,用来存放usb的设备信息。
<?xml version="1.0" encoding="utf-8"?> <span style="white-space:pre"> </span><resources> <span style="white-space:pre"> </span> <usb-device product-id="29987" vendor-id="6790" /> <span style="white-space:pre"> </span></resources>其中的ProductID跟VendorID是你要进行通信的USB设备的供应商ID(VID)和产品识别码(PID),具体数值多少可以在接下来的枚举设备方法里面得知。
新建完文件,我们自然要引用他,即在manifest.xml文件中声明。在要启动的Activity里添加 <meta-data />标签。
<activity android:name=".MainActivity" android:label="@string/app_name" android:launchMode="singleTask" > <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>此外,我在此Activity中添加一个action为USB_DEVICE_ATTACHED的inten-filter,只是为了在不打开应用的时候监听USB设备的插入,当插入USB设备,则自动打开该应用。并且,为了防止我在已打开应用的的时候,插入USB设备他再次打开一个应用,所以我设置其启动模式为singleTask。
3、枚举(发现)并获取相应的USB设备(相关USB类:UsbManager ,UsbDevice)
不管进行什么通信,我们首先该做的应是获取相应的设备。此处,应是利用UsbManager
类获取插入到此Andriod手机上的USB设备,用UsbDevice类表示。
public UsbDevice EnumerateDevice(){ mUsbmanager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE); mPendingIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(mString), 0); HashMap<String, UsbDevice> deviceList = mUsbmanager.getDeviceList(); if(deviceList.isEmpty()) { Toast.makeText(mContext, "No Device Or Device Not Match", Toast.LENGTH_LONG).show(); return null; } Iterator<UsbDevice> localIterator = deviceList.values().iterator(); while(localIterator.hasNext()) { UsbDevice localUsbDevice = localIterator.next(); for(int i = 0; i < DeviceCount; ++i) { // Log.d(TAG, "DeviceCount is " + DeviceCount); if(String.format("%04x:%04x", new Object[]{Integer.valueOf(localUsbDevice.getVendorId()), Integer.valueOf(localUsbDevice.getProductId()) }).equals(DeviceNum.get(i))) { IntentFilter filter = new IntentFilter(mString); filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); mContext.registerReceiver(mUsbReceiver, filter); BroadcastFlag = true; return localUsbDevice; } else { Log.d(TAG, "String.format not match"); } } } return null; }4、打开USB设备
此处较为复杂,其实打开设备只是一个概括性的动作,实际上他还分好几个动作,不然直接打开设备的话无法通信。
打开设备方法:
public synchronized void OpenUsbDevice(UsbDevice mDevice){ Object localObject; UsbInterface intf; if(mDevice == null) return; intf = getUsbInterface(mDevice); if((mDevice != null) && (intf != null)) { localObject = this.mUsbmanager.openDevice(mDevice); if(localObject != null) { if(((UsbDeviceConnection)localObject).claimInterface(intf, true)) { this.mUsbDevice = mDevice; this.mDeviceConnection = ((UsbDeviceConnection)localObject); this.mInterface = intf; if(!enumerateEndPoint(intf)) return; Toast.makeText(mContext, "Device Has Attached to Android", Toast.LENGTH_LONG).show(); if(READ_ENABLE == false){ READ_ENABLE = true; readThread = new read_thread(mBulkInPoint, mDeviceConnection); readThread.start(); } return; } } } }4.1 获取USB设备接口
获取到UsbDevice之后,我们在该设备上要获取相应的UsbInterface。
private UsbInterface getUsbInterface(UsbDevice paramUsbDevice) { if(this.mDeviceConnection != null) { if(this.mInterface != null) { this.mDeviceConnection.releaseInterface(this.mInterface); this.mInterface = null; } this.mDeviceConnection.close(); this.mUsbDevice = null; this.mInterface = null; } if(paramUsbDevice == null) return null; for (int i = 0; i < paramUsbDevice.getInterfaceCount(); i++) { UsbInterface intf = paramUsbDevice.getInterface(i); if (intf.getInterfaceClass() == 0xff && intf.getInterfaceSubclass() == 0x01 && intf.getInterfaceProtocol() == 0x02) { return intf; } } return null; }4.2 分配端点,IN | OUT|
获取到USB设备接口之后,我们还不能进行USB通信,USB通信需要使用UsbEndpoint端点。
private boolean enumerateEndPoint(UsbInterface sInterface){ if(sInterface == null) return false; for(int i = 0; i < sInterface.getEndpointCount(); ++i) { UsbEndpoint endPoint = sInterface.getEndpoint(i); if(endPoint.getType() == UsbConstants.USB_ENDPOINT_XFER_BULK && endPoint.getMaxPacketSize() == 0x20) { if(endPoint.getDirection() == UsbConstants.USB_DIR_IN) { mBulkInPoint = endPoint; } else { mBulkOutPoint = endPoint; } this.mBulkPacketSize = endPoint.getMaxPacketSize(); } else if(endPoint.getType() == UsbConstants.USB_ENDPOINT_XFER_CONTROL) { mCtrlPoint = endPoint; } } return true; }其中In端点mBulkInPoint是输入端点,是要来读数据的,Out端点mBulkOutPoint是输出端点,是写数据用的。
4.3
打开并连接USB设备
此处跟前面获取端点是可以互换顺序的。但从理解角度来说,我还是倾向于把打开设备放在最后,因为一般都是把配置方面都做好了,才去连接设备的。
此处直接使用UsbManager的方法进行连接:mUsbmanager.openDevice(mDevice);
到了这里,一般的即可进行USB数据的读写了。
5、读写USB数据
读数据:使用 mConn.bulkTransfer()方法,mBulkInPoint端点。另外,串口是的缓存很小的,最大也就几K,可认为几乎没有,具体大小取决于串口设备端的MCU,所以我们必须开一个线程去不断循环读取数据,不然倘若读取速度小于设备端的发送速度,这样就会导致数据丢失。
private class read_thread extends Thread { UsbEndpoint endpoint; UsbDeviceConnection mConn; read_thread(UsbEndpoint point, UsbDeviceConnection con){ endpoint = point; mConn = con; this.setPriority(Thread.MAX_PRIORITY); } public void run() { while(READ_ENABLE == true) { while(totalBytes > (maxnumbytes - 63)) { try { Thread.sleep(5); } catch (InterruptedException e) {e.printStackTrace();} } synchronized(ReadQueueLock) { if(endpoint != null) { readcount = mConn.bulkTransfer(endpoint, usbdata, 64, ReadTimeOutMillis); if(readcount > 0) { for(int count = 0; count< readcount; count++) { readBuffer[writeIndex] = usbdata[count]; writeIndex++; writeIndex %= maxnumbytes; } if(writeIndex >= readIndex) totalBytes = writeIndex-readIndex; else totalBytes = (maxnumbytes-readIndex)+writeIndex; } } } } } }当要取数据的时候,直接去取readBuffer数组的数据即可。
写数据:也是使用 mConn.bulkTransfer()方法,但是却用的mBulkOutPoint端点。
public int WriteData(byte[] buf, int length, int timeoutMillis) { int offset = 0; int HasWritten = 0; int odd_len = length; if(this.mBulkOutPoint == null) return -1; while(offset < length) { synchronized(this.WriteQueueLock) { int mLen = Math.min(odd_len, this.mBulkPacketSize); byte[] arrayOfByte = new byte[mLen]; if(offset == 0) { System.arraycopy(buf, 0, arrayOfByte, 0, mLen); } else { System.arraycopy(buf, offset, arrayOfByte, 0, mLen); } HasWritten = this.mDeviceConnection.bulkTransfer(this.mBulkOutPoint, arrayOfByte, mLen, timeoutMillis); if(HasWritten < 0) { return -2; } else { offset += HasWritten; odd_len -= HasWritten; // Log.d(TAG, "offset " + offset + " odd_len " + odd_len); } } } return offset; }到了这里,一般的读写已可以了。网上的文章一般也就说到这一步。但是如同开头所说,并没有解决串口设置的问题,此时很大可能会接收到错误的数据,这不是我们所希望的。此时,就需要我们的下一步了。
6、串口配置
在我们的这个CH340单转接芯片里,我们采用的是mDeviceConnection.controlTransfer()这个方法来进行配置。
6.1 :初始化串口:
public boolean UartInit(){ int ret; int size = 8; byte[] buffer = new byte[size]; Uart_Control_Out(UartCmd.VENDOR_SERIAL_INIT, 0x0000, 0x0000); ret = Uart_Control_In(UartCmd.VENDOR_VERSION, 0x0000, 0x0000, buffer, 2); if(ret < 0) return false; Uart_Control_Out(UartCmd.VENDOR_WRITE, 0x1312, 0xD982); Uart_Control_Out(UartCmd.VENDOR_WRITE, 0x0f2c, 0x0004); ret = Uart_Control_In(UartCmd.VENDOR_READ, 0x2518, 0x0000, buffer, 2); if(ret < 0) return false; Uart_Control_Out(UartCmd.VENDOR_WRITE, 0x2727, 0x0000); Uart_Control_Out(UartCmd.VENDOR_MODEM_OUT, 0x00ff, 0x0000); return true; }
public int Uart_Control_Out(int request, int value, int index) { int retval = 0; retval = mDeviceConnection.controlTransfer(UsbType.USB_TYPE_VENDOR | UsbType.USB_RECIP_DEVICE | UsbType.USB_DIR_OUT, request, value, index, null, 0, DEFAULT_TIMEOUT); return retval; }
public int Uart_Control_In(int request, int value, int index, byte[] buffer, int length) { int retval = 0; retval = mDeviceConnection.controlTransfer(UsbType.USB_TYPE_VENDOR | UsbType.USB_RECIP_DEVICE | UsbType.USB_DIR_IN, request, value, index, buffer, length, DEFAULT_TIMEOUT); return retval; }
6.2:配置串口参数(主要是波特率,数据位,停止位,奇偶校验位以及流控)
public boolean SetConfig(int baudRate, byte dataBit, byte stopBit, byte parity, byte flowControl){ int value = 0; int index = 0; char valueHigh = 0, valueLow = 0, indexHigh = 0, indexLow = 0; switch(parity) { case 0: /*NONE*/ valueHigh = 0x00; break; case 1: /*ODD*/ valueHigh |= 0x08; break; case 2: /*Even*/ valueHigh |= 0x18; break; case 3: /*Mark*/ valueHigh |= 0x28; break; case 4: /*Space*/ valueHigh |= 0x38; break; default: /*None*/ valueHigh = 0x00; break; } if(stopBit == 2) { valueHigh |= 0x04; } switch(dataBit) { case 5: valueHigh |= 0x00; break; case 6: valueHigh |= 0x01; break; case 7: valueHigh |= 0x02; break; case 8: valueHigh |= 0x03; break; default: valueHigh |= 0x03; break; } valueHigh |= 0xc0; valueLow = 0x9c; value |= valueLow; value |= (int)(valueHigh << 8); switch(baudRate) { case 50: indexLow = 0; indexHigh = 0x16; break; case 75: indexLow = 0; indexHigh = 0x64; break; case 110: indexLow = 0; indexHigh = 0x96; break; case 135: indexLow = 0; indexHigh = 0xa9; break; case 150: indexLow = 0; indexHigh = 0xb2; break; case 300: indexLow = 0; indexHigh = 0xd9; break; case 600: indexLow = 1; indexHigh = 0x64; break; case 1200: indexLow = 1; indexHigh = 0xb2; break; case 1800: indexLow = 1; indexHigh = 0xcc; break; case 2400: indexLow = 1; indexHigh = 0xd9; break; case 4800: indexLow = 2; indexHigh = 0x64; break; case 9600: indexLow = 2; indexHigh = 0xb2; break; case 19200: indexLow = 2; indexHigh = 0xd9; break; case 38400: indexLow = 3; indexHigh = 0x64; break; case 57600: indexLow = 3; indexHigh = 0x98; break; case 115200: indexLow = 3; indexHigh = 0xcc; break; case 230400: indexLow = 3; indexHigh = 0xe6; break; case 460800: indexLow = 3; indexHigh = 0xf3; break; case 500000: indexLow = 3; indexHigh = 0xf4; break; case 921600: indexLow = 7; indexHigh = 0xf3; break; case 1000000: indexLow = 3; indexHigh = 0xfa; break; case 2000000: indexLow = 3; indexHigh = 0xfd; break; case 3000000: indexLow = 3; indexHigh = 0xfe; break; default: // default baudRate "9600" indexLow = 2; indexHigh = 0xb2; break; } index |= 0x88 |indexLow; index |= (int)(indexHigh << 8); Uart_Control_Out(UartCmd.VENDOR_SERIAL_INIT, value, index); if(flowControl == 1) { Uart_Tiocmset(UartModem.TIOCM_DTR | UartModem.TIOCM_RTS, 0x00); } return true; }这样,串口已经初始化,以及配置好相关的波特率之类的,就可以进行正确的数据收发了,就可以实现我们的USB--UART串口通信了。
7、关闭设备
当然,最后不要忘记关闭设备,释放资源。
public synchronized void CloseDevice() { try { Thread.sleep(10); } catch (Exception e) { } if (this.mDeviceConnection != null) { if (this.mInterface != null) { this.mDeviceConnection.releaseInterface(this.mInterface); this.mInterface = null; } this.mDeviceConnection.close(); } if (this.mUsbDevice != null) { this.mUsbDevice = null; } if (this.mUsbmanager != null) { this.mUsbmanager = null; } if (READ_ENABLE == true) { READ_ENABLE = false; } /* * No need unregisterReceiver */ if (BroadcastFlag == true) { this.mContext.unregisterReceiver(mUsbReceiver); BroadcastFlag = false; } // System.exit(0); }
本文主要参考CH34X系列厂商提供的官方开发手册以及demo,有兴趣的朋友可以直接前往官网下载相关资料。 http://www.wch.cn/download/CH341SER_ANDROID_ZIP.html
本人知识有限,如果有说的不对的地方,欢迎留言指点,大家一起交流。
另外亦可以加Q交流,QQ:469325534
。
附源代码下载地址:http://download.csdn.net/detail/ever_gz/9250423。
相关文章推荐
- android MotionEvent的相关的类的介绍
- android开发中应该注意的问题
- Android5.0 SharedElement的使用
- 挨踢人的脚步(2015.11.05)
- Android SlidingMenu 使用详解
- 初识Android中的IPC
- No resource found that matches the given name 'android:Theme.Holo.Light'
- Android Studio中NDK开发
- android 6.0 Runtime Permissions Check
- Android tips
- Android基础入门教程——8.3.15 Paint API之——Typeface(字型)
- Android对话框
- 【Android开发—智能家居系列】(四):UDP通信发送指令
- Android事件拦截/分发/响应 机制
- 【Android开发—智能家居系列】(三):手机连接WIFI模块
- Android IOS WebRTC 音视频开发总结(五十)-- 技术服务如何定价?
- Android 布局样式和主题
- Android开发手记(22) 传感器的使用
- Android案例(1)——美女拼图小游戏
- android手势操作事件处理