代码说明 - matianfu/adk2012 GitHub Wiki

Android Open Accessory

Android Open Accessory (以下简称AOA)是Google为Android系统提供的一种外设通讯机制;该机制可以通过蓝牙或者USB接口和外设通讯,无须特殊系统权限,包括root和adb debug。

Android提供标准的Accessory API用于访问外设。

采用USB通讯时,外部设备的USB角色为Host,Android平板或手机为Device,与Android设备连接电脑时的角色定义相同,于USB OTG不同。

Google官方提供了Android Open Accessory Protocol的定义:

https://source.android.com/accessories/protocol.html

Google官方提供了adk2012开发套件(包含Android端和Accessory端的源码,Accessory的参考设计为Arduino平台):

http://developer.android.com/tools/adk/index.html

AOA的系统实现(USB部分)与开发要点

Android在Linux内核一级提供了一个USB Gadget驱动;

Gadget是USB Device角色的意思,对应USB Host,因为Device一词在内核驱动中广泛使用,所以开发者们用Gadget一词来表示该含义以避免混淆;

Android的USB Gadget驱动是一个复合驱动(Composite);Android的所有USB Gadget角色功能都通过该驱动封装,包含但不限于AOA,adb Debug,模拟U盘,MTP设备,PTP设备,USB CDC Ethernet/RNDIS设备(用于PC共享Android上网)等等;但OTG功能不是Gadget驱动的一部分,OTG时Android平板工作在Host模式下,使用Host驱动;Android应用层的USB Manager API支持这两种工作模式,但不能同时支持,取决于Android平板的外部连接设备种类和方式。

Android的USB Gadget驱动在每次系统USB相关设置修改时都会先与外部设备断开连接,然后修改USB Composite Device的配置,再重新连接设备;所以在应用层,当用户更改了系统的USB配置时,例如:勾选或者去掉勾选ADB Debug选项,切换MTP模式和PTP模式,允许或禁止USB Tethering(共享上网),AOA应用程序都会收到连接断开和重新连接的消息,应用程序应该及时和正确处理。

Android在Linux内核里提供了USB Accessory的Gadget驱动,该驱动会在发现Accessory设备连接时,在文件系统内创建/dev/usb_accessory设备文件节点,并通知Android Framework;此后Android Framework通过系统Intent通知和启动相应的App程序,App可以通过USB Manager提供的Accessory API获得ParcelFileDescriptor类的文件描述符,通过该描述符获得FileDescriptor类的文件描述符,再使用该文件描述符创建InputStream和OutputStream,然后就可以执行和Accessory设备之间的读写过程。

需要强调的是:虽然App获得的是Stream接口,但是该Stream接口的Java/JNI/Native Code的实现并非Stream逻辑,而是包逻辑;即每次读写数据传输都是直接按USB底层包协议传输;

USB传输在逻辑上是双向双工的,但是USB底层的实际传输是由Host一端轮循的半双工逻辑;如果Host一端不去读取数据,Device永远不会主动把数据发送出去;如果Host一端向Device一端写入数据时,Device一端没有设备接收者,则写入永远无法成功;

Android的Accessory内核以及Framework框架里沿用了这个逻辑,整个i/o通路上没有任何缓冲机制;这意味着:

  1. 如果Android App执行OutputStream.write(),而Accessory一端没有执行Receive,则该调用被阻塞,永远不会返回;同时Android没有实现Thread的Interrupt方法,这意味着在这种场景下执行了该调用的Thread会永久被阻塞;

  2. 如果Android App执行InputStream.read(),而Accessory一端没有执行Send,则该调用被阻塞,永远不会返回;同时针对Accessory的InputStream的available()方法没有被实现,无法用于防阻塞的读取模式;

  3. USB底层数据包大小为64byte,write()超过64byte的数据是可能的,但在没有必要的情况下应该避免;write会写完所有数据才会返回;该api不会象标准的java stream api那样有可能写完部分数据返回,只会全部写完返回;

  4. read()不会返回超过64byte的结果,而且read()返回的一定是Accessory一端发送的一个完整的包;不会出现把包截断分多次返回的情况;

简言之,开发者需要把write()和read()方法理解成底层的USB包读写API,而不是标准的Java Stream API;而且仅使用基础的InputStream和OutputStream类进行读写,不使用其他子类,例如BufferedInputStream;也不使用其他基于Stream构建的Reader/Scanner类,例如BufferReader,这些类提供的高级方法绝大多数都无法正常工作;

在工作方式上,开发者应该使用本文档提供的Command-Response模式实现与外设的小数据量交换;不应采用Java常用的单独开辟read/write线程,阻塞式读写的方法;

由于Android框架层的一个bug,read() api阻塞时,如果进程被系统kill;则再也无法通过USB Manager重新获取Accessory的文件描述符,除非外部USB连接物理插拔一次(USB VBUS断开),或者重启Android设备,或者重启外部设备;因此在Google官方提供修复之前,使用AOA,单独开辟线程blocking read()的设计模式是绝对禁止的。

本文档设计的工作方式和外部设备的代码实现,保证Android设备在执行Read的时候一定可以获得数据返回;避免无用户干预无法自动恢复的错误发生。

AOA的设备握手过程

绝大多数Android设备,在缺省情况下都不挂载Accessory驱动;在Accessory与Android设备建立USB连接时;Accessory会通过握手协议查询该设备是否为Android设备且具有AOA支持,如果获得正确应答,Accessory会向Android设备发出切换到AOA模式的请求,Android设备会执行请求,将USB切换到AOA模式;在这个过程中,USB连接会出现一次逻辑插拔,USB Host一端会重新枚举设备。

在握手过程中,Accessory会向Android提供AOA约定的描述信息,其中有三个信息是Android系统用于绑定Accessory设备与App的;分别是:

  1. manufacturer
  2. model
  3. version

Android系统根据这三个字符串匹配相应的App。

如果系统内无任何App可以匹配Accessory设备发来的握手信息;则Android设备会弹出一个对话框,向用户提供Accessory设备发送过来的描述信息和URL信息,用户可以点击URL访问它指向的Web页面。

如果系统内有App可以匹配Accessory设备发来的握手信息;则Android会弹出一个对话框询问用户是否立刻启动该App;如果用户选择OK则启动该App;同时该对话框提供一个勾选框,勾选之后每次Accessory设备连接后会自动启动该App;

应该要求用户勾选该对话框,否则App启动后向USB Manager获取Accessory设备后可能因为Permission问题无法打开文件描述符建立通讯连接;

Android App Manifest文件绑定方法

Android App只有一种方式绑定Accessory的握手信息;按照Android的Accessory系统设计要求,在App的manifest文件中提供相应的信息,如下例所示:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xml>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.apps.adk2"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="12"
        android:targetSdkVersion="22" />
    <uses-feature
        android:name="android.hardware.usb.accessory"
        android:required="true" />
    <application
        android:allowBackup="true"
        android:hardwareAccelerated="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_short_name" >
        <activity
            android:name=".activity.HomeActivity"
            android:label="@string/app_short_name"
            android:launchMode="singleTop"
            android:screenOrientation="portrait" >
            <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_ACCESSORY_ATTACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
                android:resource="@xml/usb_accessory_filter" />
        </activity>
    </application>
</manifest>

以下内容是必须的:

minSdkVersion属性必须为12或以上,才有AOA支持;

    <uses-sdk
        android:minSdkVersion="12"
        android:targetSdkVersion="22" />

必须如下所示声名App需要使用USB Accessory Feature。

    <uses-feature
        android:name="android.hardware.usb.accessory"
        android:required="true" />

必须有一个Activity声明具有如下属性:

  1. launchMode包含singleTop,这是程序在已经启动后,活得Accessory设备插入的唯一方法;
  2. 声明一个intent filter,绑定USB_ACCESSORY_ATTACHED intent;
  3. 必须提供一个meta-data标签,为USB_ACCESSORY_ATTACHED intent提供meta-data;
        <activity
            ...
            android:launchMode="singleTop"
            ...
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
                android:resource="@xml/usb_accessory_filter" />
        </activity>

对应的meta-data文件如下,其中的三个握手信息字串必须与Accessory设备提供的精确一致,否则无法绑定;

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-accessory
        manufacturer="Actnova"
        model="DemoKit"
        version="1.0" />
</resources>

获得Accessory设备连接的通知

如果App尚未启动,获得设备连接消息的唯一方法是通过上述Manifest文件里声明Intent;

如果App已经启动,获得设备连接消息的唯一方法是仍然是通过上述Manifest文件里声明的Intent;但此时程序已经启动,将获得一个新的Intent,App必须实现onNewIntent方法,代码示例如下:

	@Override
	protected void onNewIntent(Intent intent) {

		Log.i(TAG, "- onNewIntent");

		String action = intent.getAction();
		if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED.equals(action)) {
			/*
			 * When this happens, it means the previous stream are invalid.
			 */
			if (mAccessory != null) {
				if (mFileDescriptor != null) {
					closeAccessory();
				}
			}

			mAccessory = null;

			UsbAccessory accessory = (UsbAccessory) intent
					.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
			if (accessory != null) {
				mAccessory = accessory;
				openAccessory();
				registerDetachReceiver();
			}
		}
	}

接受USB连接的Intent的Activity不必一定是MainActivity;也可以单独用一个Activity来接受设备连接时的Intent;在Google官方的ADK2012代码示例中,使用了单独的UsbAccessoryActivity来负责接受Intent,然后它再抛出新的Intent给HomeActivity;

ADK2012的UsbAccessoryActivity.java的源代码如下:

// receive USB_DEVICE_ATTACHED events and launch the main activity
public final class USBAccessoryActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		Intent i = (new Intent(this, HomeActivity.class));
		i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
				| Intent.FLAG_ACTIVITY_CLEAR_TOP);
		startActivity(i);

		finish();
	}
}

注意:使用BroadcastReceiver是无法获得Accessory设备连接的信息的,只能通过Intent。

获得Accessory设备断开连接的通知

与获得设备连接通知刚好相反,获得Accessory设备断开连接的通知只能通过BroadcastReceiver;在获得通知后应该判断Intent类型,比较accessory对象,之后执行关闭accessory的操作;

示例代码如下:

	private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
			String action = intent.getAction();
			if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
				// UsbAccessory accessory = UsbManager.getAccessory(intent);
				UsbAccessory accessory = (UsbAccessory) intent
						.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
				if (accessory != null && accessory.equals(mAccessory)) {
					Log.i(TAG, "Accessory Detached");
					closeAccessory();
					unregisterDetachReceiver();
					mAccessory = null;
				}
			}
		}
	};

注册该BroadcastReceiver的示例代码如下:

	private void registerDetachReceiver() {
		IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
		filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
		registerReceiver(mUsbReceiver, filter);
	}

应该在成功打开accessory设备之后及时用上述方法注册DETACH事件的BroadcastReceiver;有处:一是程序启动时,轮循USB Manager获得Accessory之后,另一处是onNewIntent,在程序运行时Accessory连接;

打开与关闭Accessory

APP程序必须持续持有(保存在成员中)的对象是两个:

  1. Accessory对象,从USB Manager获得;
  2. 从Accessory获得的ParcelFileDescriptor类型的文件描述符;

示例代码如下:

	/**
	 * Open Accessory actually open file descriptor, establish a connection. In
	 * framework, /dev/usb_accessory will be opened.
	 */
	private void openAccessory() {

		if (mAccessory == null)
			return;

		mFileDescriptor = getUsbManager().openAccessory(mAccessory);

		if (mFileDescriptor != null) {
			mLabel1.setText("Accessory Connected");
			consolePuts("");
			consolePuts("");
			consolePuts("Connected");
			Log.i(TAG, "openAccessory success");
		} else {
			Log.i(TAG, "!!! openAccessory fail");
		}
	}

	/**
	 * Close open file descriptor for usb accessory. This function is crucial
	 * for maintaining robust connection. Failing to call this function before
	 * application exit (including low mem kill) will result in failure in
	 * subsequent reopen. No way to recover without usb reset or system reboot.
	 */
	private void closeAccessory() {

		if (mFileDescriptor != null) {
			try {
				mFileDescriptor.close();
				mLabel1.setText("Accessory Disconnected");
				consolePuts("Disconnected");	
				Log.i(TAG, "closeAccessory success");
			} catch (IOException e) {
				Log.i(TAG, "!!! mFileDescriptor closed with IOException, "
						+ e.getClass().toString() + ", " + e.getMessage());
			} finally {
				mFileDescriptor = null;
			}
		}
	}

建立通讯

从ParcelFileDescriptor获得真正的FileDescriptor,然后建立InputStream和OutputStream即可;

		FileDescriptor fd = mFileDescriptor.getFileDescriptor();
		FileInputStream is = new FileInputStream(fd);
		FileOutputStream os = new FileOutputStream(fd);

Activity生命周期问题

如前所述,因为Android的系统Bug,如果FileInputStream.read()方法在执行时,进程被Kill,会导致必须人工干预重新插拔USB才能恢复Accessory设备获取;必须避免这种情况的发生;

如果采用Activity而不是Service来实现AOA通讯,我们推荐的做法如DemoKit代码所示,在onStart()内打开Accessory,在onStop()时关闭;InputStream和OutputStream只在读写操作时建立,这样最大程度的避免程序切换到后台、挂起、前台弹出其他程序时,遇到上述Bug问题。

Command-Response模式

目前DemoKit对通讯的约定是,逻辑上,Android App是命令方,Accessory设备是响应方;任何读写过程都由App发起,先使用write写入命令(Command),然后立刻用read读取返回信息(Response);

在Accessory一端的程序实现是:

  1. 保持永久的读取状态,保证在任何时候App发起write操作,都可以立刻返回;
  2. 在获得App发出的命令后,立刻返回数据,保证App随后执行的read命令可以立刻返回,不会被阻塞;

依次保证双方通讯稳定,设备连接和断开连接处理可靠;且App的代码实现简单。

⚠️ **GitHub.com Fallback** ⚠️