aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Crossan <kcrossan@google.com>2016-11-28 18:40:43 -0800
committerKevin Crossan <kcrossan@google.com>2016-11-29 13:59:33 -0800
commitd428549b58b2df5015bff81d79747265ee8be536 (patch)
tree463f90628b0abd1db7225d311cfee9ede3103fc4
parentee412315d30eb1b8a0e8a4ea232f78ecc53b7486 (diff)
downloadCar-d428549b58b2df5015bff81d79747265ee8be536.tar.gz
Implement AOAP USB handler with simplified probing.
The existing USB handler, part of the Kitchen Sink application, was unreliable and hard to follow. This new handler simplifies the code flow and only probes new USB devices by querying whether they support AOAP by sending a USB control message. Further tests (such as switching the device into AOAP) are not done by this app. This new handler also attempts to handle any already connected AOAP devices on system boot. This enables projection to start automatically for compatible devices. NOTE: because devices are not switched into AOAP mode during probing, this means that IUsbAoapSupportCheckService.isDeviceSupported() is now called with a UsbDevice that is _not_ in AOAP mode. Fixes: 33185277 Test: Plugged the following MDs in and projection started: N5, N5X, N6P, Pixel, Galaxy Note Edge, Moto X gen2, Xperia Z5, HTC M8 Test: Plugged in a phone before the system booted completely, and projection started Change-Id: Ice200f661bd85e6eebc97c95bcd23910d4dc25e6
-rw-r--r--car-usb-handler/Android.mk35
-rw-r--r--car-usb-handler/AndroidManifest.xml33
-rw-r--r--car-usb-handler/proguard.flags2
-rw-r--r--car-usb-handler/res/drawable-hdpi/ic_launcher.pngbin0 -> 9397 bytes
-rw-r--r--car-usb-handler/res/drawable-ldpi/ic_launcher.pngbin0 -> 2729 bytes
-rw-r--r--car-usb-handler/res/drawable-mdpi/ic_launcher.pngbin0 -> 5237 bytes
-rw-r--r--car-usb-handler/res/drawable-xhdpi/ic_launcher.pngbin0 -> 14383 bytes
-rw-r--r--car-usb-handler/res/layout/usb_handler_row.xml30
-rw-r--r--car-usb-handler/res/layout/usb_host.xml40
-rw-r--r--car-usb-handler/res/values/strings.xml29
-rw-r--r--car-usb-handler/res/xml/usb_manager_prefs.xml18
-rw-r--r--car-usb-handler/src/android/car/usb/handler/AoapInterface.java146
-rw-r--r--car-usb-handler/src/android/car/usb/handler/BootUsbScanner.java40
-rw-r--r--car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java670
-rw-r--r--car-usb-handler/src/android/car/usb/handler/UsbDevicePreference.java71
-rw-r--r--car-usb-handler/src/android/car/usb/handler/UsbDeviceSettings.java135
-rw-r--r--car-usb-handler/src/android/car/usb/handler/UsbHostController.java241
-rw-r--r--car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java191
-rw-r--r--car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java196
-rw-r--r--car-usb-handler/src/android/car/usb/handler/UsbUtil.java91
-rw-r--r--car_product/build/car.mk1
21 files changed, 1969 insertions, 0 deletions
diff --git a/car-usb-handler/Android.mk b/car-usb-handler/Android.mk
new file mode 100644
index 0000000000..c0508540a5
--- /dev/null
+++ b/car-usb-handler/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# 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.
+#
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CarUsbHandler
+
+# Each update should be signed by OEMs
+LOCAL_CERTIFICATE := platform
+LOCAL_PRIVILEGED_MODULE := true
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_JAVA_LIBRARIES += android.car
+
+include $(BUILD_PACKAGE)
diff --git a/car-usb-handler/AndroidManifest.xml b/car-usb-handler/AndroidManifest.xml
new file mode 100644
index 0000000000..be64f82d51
--- /dev/null
+++ b/car-usb-handler/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ package="android.car.usb.handler" >
+ <uses-sdk android:minSdkVersion="25" />
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.MANAGE_USB" />
+ <application android:label="@string/app_name" android:icon="@drawable/ic_launcher"
+ android:directBootAware="true" >
+ <activity android:name=".UsbHostManagementActivity"
+ android:theme="@android:style/Theme.Material.Light.Dialog"
+ android:launchMode="singleTop" />
+ <receiver android:name=".BootUsbScanner" >
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/car-usb-handler/proguard.flags b/car-usb-handler/proguard.flags
new file mode 100644
index 0000000000..565df26b8d
--- /dev/null
+++ b/car-usb-handler/proguard.flags
@@ -0,0 +1,2 @@
+-verbose
+-keep @com.android.internal.annotations.VisibleForTesting class *
diff --git a/car-usb-handler/res/drawable-hdpi/ic_launcher.png b/car-usb-handler/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..96a442e5b8
--- /dev/null
+++ b/car-usb-handler/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/car-usb-handler/res/drawable-ldpi/ic_launcher.png b/car-usb-handler/res/drawable-ldpi/ic_launcher.png
new file mode 100644
index 0000000000..99238729d8
--- /dev/null
+++ b/car-usb-handler/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/car-usb-handler/res/drawable-mdpi/ic_launcher.png b/car-usb-handler/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..359047dfa4
--- /dev/null
+++ b/car-usb-handler/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/car-usb-handler/res/drawable-xhdpi/ic_launcher.png b/car-usb-handler/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..71c6d760f0
--- /dev/null
+++ b/car-usb-handler/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/car-usb-handler/res/layout/usb_handler_row.xml b/car-usb-handler/res/layout/usb_handler_row.xml
new file mode 100644
index 0000000000..cc34e5ec40
--- /dev/null
+++ b/car-usb-handler/res/layout/usb_handler_row.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal" >
+ <ImageView
+ android:id="@+id/usb_handler_icon"
+ android:layout_width="60dp"
+ android:layout_height="60dp"
+ android:padding="5dp" />
+ <TextView
+ android:id="@+id/usb_handler_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+</LinearLayout>
diff --git a/car-usb-handler/res/layout/usb_host.xml b/car-usb-handler/res/layout/usb_host.xml
new file mode 100644
index 0000000000..fbcc608e5f
--- /dev/null
+++ b/car-usb-handler/res/layout/usb_host.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <LinearLayout
+ android:id="@+id/usb_handlers_progress"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:gravity="center">
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+ <TextView
+ android:text="@+string/usb_resolving_handlers"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+ </LinearLayout>
+ <ListView
+ android:id="@+id/usb_handlers_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+</LinearLayout>
diff --git a/car-usb-handler/res/values/strings.xml b/car-usb-handler/res/values/strings.xml
new file mode 100644
index 0000000000..2242648941
--- /dev/null
+++ b/car-usb-handler/res/values/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ 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.
+-->
+<resources>
+ <string name="app_name">USB Handler</string>
+
+ <!-- USB Manager Settings -->
+ <string name="usb_title">USB Devices settings</string>
+ <string name="usb_description">Customize your USB Devices settings</string>
+ <string name="usb_available_devices">Connected devices</string>
+ <string name="usb_saved_devices">Saved devices</string>
+ <string name="usb_pref_delete_title">Remove handling app for USB device</string>
+ <string name="usb_pref_delete_message">Are you sure you wan to delete dafault handling app for %1$s?</string>
+ <string name="usb_pref_delete_yes">Yes</string>
+ <string name="usb_pref_delete_cancel">Cancel</string>
+ <string name="usb_resolving_handlers">Getting supported handlers</string>
+</resources>
diff --git a/car-usb-handler/res/xml/usb_manager_prefs.xml b/car-usb-handler/res/xml/usb_manager_prefs.xml
new file mode 100644
index 0000000000..b0901240e9
--- /dev/null
+++ b/car-usb-handler/res/xml/usb_manager_prefs.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ 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.
+ -->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+</PreferenceScreen>
diff --git a/car-usb-handler/src/android/car/usb/handler/AoapInterface.java b/car-usb-handler/src/android/car/usb/handler/AoapInterface.java
new file mode 100644
index 0000000000..e4d843f752
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/AoapInterface.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * <p>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
+ *
+ * <p>http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * <p>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 android.car.usb.handler;
+
+import android.hardware.usb.UsbConstants;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.util.Log;
+import java.io.IOException;
+
+final class AoapInterface {
+ /**
+ * Use Google Vendor ID when in accessory mode
+ */
+ public static final int USB_ACCESSORY_VENDOR_ID = 0x18D1;
+
+ /**
+ * Product ID to use when in accessory mode
+ */
+ public static final int USB_ACCESSORY_PRODUCT_ID = 0x2D00;
+
+ /**
+ * Product ID to use when in accessory mode and adb is enabled
+ */
+ public static final int USB_ACCESSORY_ADB_PRODUCT_ID = 0x2D01;
+
+ /**
+ * Indexes for strings sent by the host via ACCESSORY_SEND_STRING
+ */
+ public static final int ACCESSORY_STRING_MANUFACTURER = 0;
+ public static final int ACCESSORY_STRING_MODEL = 1;
+ public static final int ACCESSORY_STRING_DESCRIPTION = 2;
+ public static final int ACCESSORY_STRING_VERSION = 3;
+ public static final int ACCESSORY_STRING_URI = 4;
+ public static final int ACCESSORY_STRING_SERIAL = 5;
+
+ /**
+ * Control request for retrieving device's protocol version
+ *
+ * requestType: USB_DIR_IN | USB_TYPE_VENDOR
+ * request: ACCESSORY_GET_PROTOCOL
+ * value: 0
+ * index: 0
+ * data version number (16 bits little endian)
+ * 1 for original accessory support
+ * 2 adds HID and device to host audio support
+ */
+ public static final int ACCESSORY_GET_PROTOCOL = 51;
+
+ /**
+ * Control request for host to send a string to the device
+ *
+ * requestType: USB_DIR_OUT | USB_TYPE_VENDOR
+ * request: ACCESSORY_SEND_STRING
+ * value: 0
+ * index: string ID
+ * data zero terminated UTF8 string
+ *
+ * The device can later retrieve these strings via the
+ * ACCESSORY_GET_STRING_* ioctls
+ */
+ public static final int ACCESSORY_SEND_STRING = 52;
+
+ /**
+ * Control request for starting device in accessory mode.
+ * The host sends this after setting all its strings to the device.
+ *
+ * requestType: USB_DIR_OUT | USB_TYPE_VENDOR
+ * request: ACCESSORY_START
+ * value: 0
+ * index: 0
+ * data none
+ */
+ public static final int ACCESSORY_START = 53;
+
+ /**
+ * Max payload size for AOAP. Limited by driver.
+ */
+ public static final int MAX_PAYLOAD_SIZE = 16384;
+
+ /**
+ * Accessory write timeout.
+ */
+ public static final int AOAP_TIMEOUT_MS = 2000;
+
+ private static final String TAG = AoapInterface.class.getSimpleName();
+
+ public static int getProtocol(UsbDeviceConnection conn) {
+ byte[] buffer = new byte[2];
+ int len = conn.controlTransfer(
+ UsbConstants.USB_DIR_IN | UsbConstants.USB_TYPE_VENDOR,
+ ACCESSORY_GET_PROTOCOL, 0, 0, buffer, 2, AOAP_TIMEOUT_MS);
+ if (len != 2) {
+ return -1;
+ }
+ return (buffer[1] << 8) | buffer[0];
+ }
+
+ public static boolean isSupported(UsbDeviceConnection conn) {
+ return getProtocol(conn) >= 1;
+ }
+
+ public static void sendString(UsbDeviceConnection conn, int index, String string)
+ throws IOException {
+ byte[] buffer = (string + "\0").getBytes();
+ int len = conn.controlTransfer(
+ UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
+ ACCESSORY_SEND_STRING, 0, index,
+ buffer, buffer.length, AOAP_TIMEOUT_MS);
+ if (len != buffer.length) {
+ throw new IOException("Failed to send string " + index + ": \"" + string + "\"");
+ } else {
+ Log.i(TAG, "Sent string " + index + ": \"" + string + "\"");
+ }
+ }
+
+ public static void sendAoapStart(UsbDeviceConnection conn) throws IOException {
+ int len = conn.controlTransfer(
+ UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
+ ACCESSORY_START, 0, 0, null, 0, AOAP_TIMEOUT_MS);
+ if (len < 0) {
+ throw new IOException("Control transfer for accessory start failed: " + len);
+ }
+ }
+
+ public static boolean isDeviceInAoapMode(UsbDevice device) {
+ if (device == null) {
+ return false;
+ }
+ final int vid = device.getVendorId();
+ final int pid = device.getProductId();
+ return vid == USB_ACCESSORY_VENDOR_ID
+ && (pid == USB_ACCESSORY_PRODUCT_ID || pid == USB_ACCESSORY_ADB_PRODUCT_ID);
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/BootUsbScanner.java b/car-usb-handler/src/android/car/usb/handler/BootUsbScanner.java
new file mode 100644
index 0000000000..5255f165b9
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/BootUsbScanner.java
@@ -0,0 +1,40 @@
+package android.car.usb.handler;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbManager;
+
+public class BootUsbScanner extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // TODO: move probing of devices to a service, since AoapInterface.isSupported() could take
+ // up to 2 seconds and many USB devices could be connected.
+ UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+ for (UsbDevice device : manager.getDeviceList().values()) {
+ if (AoapInterface.isDeviceInAoapMode(device)) {
+ // This could happen if we reboot. We should try to handle this accessory.
+ handle(context, device);
+ } else {
+ UsbDeviceConnection connection = UsbUtil.openConnection(manager, device);
+ try {
+ if (AoapInterface.isSupported(connection)) {
+ handle(context, device);
+ }
+ } finally {
+ connection.close();
+ }
+ }
+ }
+ }
+
+ private void handle(Context context, UsbDevice device) {
+ Intent manageDevice = new Intent(context, UsbHostManagementActivity.class);
+ manageDevice.setAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ manageDevice.putExtra(UsbManager.EXTRA_DEVICE, device);
+ manageDevice.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(manageDevice);
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java b/car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java
new file mode 100644
index 0000000000..c0eebbea7d
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbDeviceHandlerResolver.java
@@ -0,0 +1,670 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.car.usb.handler;
+
+import android.car.IUsbAoapSupportCheckService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.XmlResourceParser;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbInterface;
+import android.hardware.usb.UsbManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Pair;
+import com.android.internal.util.XmlUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import org.xmlpull.v1.XmlPullParser;
+
+/** Resolves supported handlers for USB device. */
+public final class UsbDeviceHandlerResolver {
+ private static final String TAG = UsbDeviceHandlerResolver.class.getSimpleName();
+ private static final boolean LOCAL_LOGD = true;
+
+ /**
+ * Callbacks for device resolver.
+ */
+ public interface UsbDeviceHandlerResolverCallback {
+ /** Handlers are resolved */
+ void onHandlersResolveCompleted(
+ UsbDevice device, List<UsbDeviceSettings> availableSettings);
+ /** Device was dispatched */
+ void onDeviceDispatched();
+ }
+
+ private final UsbManager mUsbManager;
+ private final PackageManager mPackageManager;
+ private final UsbDeviceHandlerResolverCallback mDeviceCallback;
+ private final Context mContext;
+ private final HandlerThread mHandlerThread;
+ private final UsbDeviceResolverHandler mHandler;
+
+ private class DeviceContext {
+ public final UsbDevice usbDevice;
+ public final UsbDeviceConnection connection;
+ public final UsbDeviceSettings settings;
+ public final List<UsbDeviceSettings> activeDeviceSettings;
+ public final Queue<Pair<ResolveInfo, DeviceFilter>> mActiveDeviceOptions =
+ new LinkedList<>();
+
+ private volatile IUsbAoapSupportCheckService mUsbAoapSupportCheckService;
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ Log.i(TAG, "onServiceConnected: " + className);
+ mUsbAoapSupportCheckService = IUsbAoapSupportCheckService.Stub.asInterface(service);
+ mHandler.requestOnServiceConnectionStateChanged(DeviceContext.this);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName className) {
+ Log.i(TAG, "onServiceDisconnected: " + className);
+ mUsbAoapSupportCheckService = null;
+ mHandler.requestOnServiceConnectionStateChanged(DeviceContext.this);
+ }
+ };
+
+ public DeviceContext(UsbDevice usbDevice, UsbDeviceSettings settings,
+ List<UsbDeviceSettings> activeDeviceSettings) {
+ this.usbDevice = usbDevice;
+ this.settings = settings;
+ this.activeDeviceSettings = activeDeviceSettings;
+ connection = UsbUtil.openConnection(mUsbManager, usbDevice);
+ }
+ }
+
+ // This class is used to describe a USB device.
+ // When used in HashMaps all values must be specified,
+ // but wildcards can be used for any of the fields in
+ // the package meta-data.
+ private static class DeviceFilter {
+ // USB Vendor ID (or -1 for unspecified)
+ public final int mVendorId;
+ // USB Product ID (or -1 for unspecified)
+ public final int mProductId;
+ // USB device or interface class (or -1 for unspecified)
+ public final int mClass;
+ // USB device subclass (or -1 for unspecified)
+ public final int mSubclass;
+ // USB device protocol (or -1 for unspecified)
+ public final int mProtocol;
+ // USB device manufacturer name string (or null for unspecified)
+ public final String mManufacturerName;
+ // USB device product name string (or null for unspecified)
+ public final String mProductName;
+ // USB device serial number string (or null for unspecified)
+ public final String mSerialNumber;
+
+ // USB device in AOAP mode manufacturer
+ public final String mAoapManufacturer;
+ // USB device in AOAP mode model
+ public final String mAoapModel;
+ // USB device in AOAP mode description string
+ public final String mAoapDescription;
+ // USB device in AOAP mode version
+ public final String mAoapVersion;
+ // USB device in AOAP mode URI
+ public final String mAoapUri;
+ // USB device in AOAP mode serial
+ public final String mAoapSerial;
+ // USB device in AOAP mode verification service
+ public final String mAoapService;
+
+ DeviceFilter(int vid, int pid, int clasz, int subclass, int protocol,
+ String manufacturer, String product, String serialnum,
+ String aoapManufacturer, String aoapModel, String aoapDescription,
+ String aoapVersion, String aoapUri, String aoapSerial,
+ String aoapService) {
+ mVendorId = vid;
+ mProductId = pid;
+ mClass = clasz;
+ mSubclass = subclass;
+ mProtocol = protocol;
+ mManufacturerName = manufacturer;
+ mProductName = product;
+ mSerialNumber = serialnum;
+
+ mAoapManufacturer = aoapManufacturer;
+ mAoapModel = aoapModel;
+ mAoapDescription = aoapDescription;
+ mAoapVersion = aoapVersion;
+ mAoapUri = aoapUri;
+ mAoapSerial = aoapSerial;
+ mAoapService = aoapService;
+ }
+
+ DeviceFilter(UsbDevice device) {
+ mVendorId = device.getVendorId();
+ mProductId = device.getProductId();
+ mClass = device.getDeviceClass();
+ mSubclass = device.getDeviceSubclass();
+ mProtocol = device.getDeviceProtocol();
+ mManufacturerName = device.getManufacturerName();
+ mProductName = device.getProductName();
+ mSerialNumber = device.getSerialNumber();
+ mAoapManufacturer = null;
+ mAoapModel = null;
+ mAoapDescription = null;
+ mAoapVersion = null;
+ mAoapUri = null;
+ mAoapSerial = null;
+ mAoapService = null;
+ }
+
+ public static DeviceFilter read(XmlPullParser parser, boolean aoapData) {
+ int vendorId = -1;
+ int productId = -1;
+ int deviceClass = -1;
+ int deviceSubclass = -1;
+ int deviceProtocol = -1;
+ String manufacturerName = null;
+ String productName = null;
+ String serialNumber = null;
+
+ String aoapManufacturer = null;
+ String aoapModel = null;
+ String aoapDescription = null;
+ String aoapVersion = null;
+ String aoapUri = null;
+ String aoapSerial = null;
+ String aoapService = null;
+
+ int count = parser.getAttributeCount();
+ for (int i = 0; i < count; i++) {
+ String name = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ // Attribute values are ints or strings
+ if (!aoapData && "manufacturer-name".equals(name)) {
+ manufacturerName = value;
+ } else if (!aoapData && "product-name".equals(name)) {
+ productName = value;
+ } else if (!aoapData && "serial-number".equals(name)) {
+ serialNumber = value;
+ } else if (aoapData && "manufacturer".equals(name)) {
+ aoapManufacturer = value;
+ } else if (aoapData && "model".equals(name)) {
+ aoapModel = value;
+ } else if (aoapData && "description".equals(name)) {
+ aoapDescription = value;
+ } else if (aoapData && "version".equals(name)) {
+ aoapVersion = value;
+ } else if (aoapData && "uri".equals(name)) {
+ aoapUri = value;
+ } else if (aoapData && "serial".equals(name)) {
+ aoapSerial = value;
+ } else if (aoapData && "service".equals(name)) {
+ aoapService = value;
+ } else if (!aoapData) {
+ int intValue = -1;
+ int radix = 10;
+ if (value != null && value.length() > 2 && value.charAt(0) == '0'
+ && (value.charAt(1) == 'x' || value.charAt(1) == 'X')) {
+ // allow hex values starting with 0x or 0X
+ radix = 16;
+ value = value.substring(2);
+ }
+ try {
+ intValue = Integer.parseInt(value, radix);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "invalid number for field " + name, e);
+ continue;
+ }
+ if ("vendor-id".equals(name)) {
+ vendorId = intValue;
+ } else if ("product-id".equals(name)) {
+ productId = intValue;
+ } else if ("class".equals(name)) {
+ deviceClass = intValue;
+ } else if ("subclass".equals(name)) {
+ deviceSubclass = intValue;
+ } else if ("protocol".equals(name)) {
+ deviceProtocol = intValue;
+ }
+ }
+ }
+ return new DeviceFilter(vendorId, productId,
+ deviceClass, deviceSubclass, deviceProtocol,
+ manufacturerName, productName, serialNumber, aoapManufacturer,
+ aoapModel, aoapDescription, aoapVersion, aoapUri, aoapSerial,
+ aoapService);
+ }
+
+ private boolean matches(int clasz, int subclass, int protocol) {
+ return ((mClass == -1 || clasz == mClass)
+ && (mSubclass == -1 || subclass == mSubclass)
+ && (mProtocol == -1 || protocol == mProtocol));
+ }
+
+ public boolean matches(UsbDevice device) {
+ if (mVendorId != -1 && device.getVendorId() != mVendorId) {
+ return false;
+ }
+ if (mProductId != -1 && device.getProductId() != mProductId) {
+ return false;
+ }
+ if (mManufacturerName != null && device.getManufacturerName() == null) {
+ return false;
+ }
+ if (mProductName != null && device.getProductName() == null) {
+ return false;
+ }
+ if (mSerialNumber != null && device.getSerialNumber() == null) {
+ return false;
+ }
+ if (mManufacturerName != null && device.getManufacturerName() != null
+ && !mManufacturerName.equals(device.getManufacturerName())) {
+ return false;
+ }
+ if (mProductName != null && device.getProductName() != null
+ && !mProductName.equals(device.getProductName())) {
+ return false;
+ }
+ if (mSerialNumber != null && device.getSerialNumber() != null
+ && !mSerialNumber.equals(device.getSerialNumber())) {
+ return false;
+ }
+
+ // check device class/subclass/protocol
+ if (matches(device.getDeviceClass(), device.getDeviceSubclass(),
+ device.getDeviceProtocol())) {
+ return true;
+ }
+
+ // if device doesn't match, check the interfaces
+ int count = device.getInterfaceCount();
+ for (int i = 0; i < count; i++) {
+ UsbInterface intf = device.getInterface(i);
+ if (matches(intf.getInterfaceClass(), intf.getInterfaceSubclass(),
+ intf.getInterfaceProtocol())) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ // can't compare if we have wildcard strings
+ if (mVendorId == -1 || mProductId == -1
+ || mClass == -1 || mSubclass == -1 || mProtocol == -1) {
+ return false;
+ }
+ if (obj instanceof DeviceFilter) {
+ DeviceFilter filter = (DeviceFilter) obj;
+
+ if (filter.mVendorId != mVendorId
+ || filter.mProductId != mProductId
+ || filter.mClass != mClass
+ || filter.mSubclass != mSubclass
+ || filter.mProtocol != mProtocol) {
+ return false;
+ }
+ if ((filter.mManufacturerName != null && mManufacturerName == null)
+ || (filter.mManufacturerName == null && mManufacturerName != null)
+ || (filter.mProductName != null && mProductName == null)
+ || (filter.mProductName == null && mProductName != null)
+ || (filter.mSerialNumber != null && mSerialNumber == null)
+ || (filter.mSerialNumber == null && mSerialNumber != null)) {
+ return false;
+ }
+ if ((filter.mManufacturerName != null && mManufacturerName != null
+ && !mManufacturerName.equals(filter.mManufacturerName))
+ || (filter.mProductName != null && mProductName != null
+ && !mProductName.equals(filter.mProductName))
+ || (filter.mSerialNumber != null && mSerialNumber != null
+ && !mSerialNumber.equals(filter.mSerialNumber))) {
+ return false;
+ }
+ return true;
+ }
+ if (obj instanceof UsbDevice) {
+ UsbDevice device = (UsbDevice) obj;
+ if (device.getVendorId() != mVendorId
+ || device.getProductId() != mProductId
+ || device.getDeviceClass() != mClass
+ || device.getDeviceSubclass() != mSubclass
+ || device.getDeviceProtocol() != mProtocol) {
+ return false;
+ }
+ if ((mManufacturerName != null && device.getManufacturerName() == null)
+ || (mManufacturerName == null && device.getManufacturerName() != null)
+ || (mProductName != null && device.getProductName() == null)
+ || (mProductName == null && device.getProductName() != null)
+ || (mSerialNumber != null && device.getSerialNumber() == null)
+ || (mSerialNumber == null && device.getSerialNumber() != null)) {
+ return false;
+ }
+ if ((device.getManufacturerName() != null
+ && !mManufacturerName.equals(device.getManufacturerName()))
+ || (device.getProductName() != null
+ && !mProductName.equals(device.getProductName()))
+ || (device.getSerialNumber() != null
+ && !mSerialNumber.equals(device.getSerialNumber()))) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return (((mVendorId << 16) | mProductId)
+ ^ ((mClass << 16) | (mSubclass << 8) | mProtocol));
+ }
+
+ @Override
+ public String toString() {
+ return "DeviceFilter[mVendorId=" + mVendorId + ",mProductId=" + mProductId
+ + ",mClass=" + mClass + ",mSubclass=" + mSubclass
+ + ",mProtocol=" + mProtocol + ",mManufacturerName=" + mManufacturerName
+ + ",mProductName=" + mProductName + ",mSerialNumber=" + mSerialNumber + "]";
+ }
+ }
+
+ public UsbDeviceHandlerResolver(UsbManager manager, Context context,
+ UsbDeviceHandlerResolverCallback deviceListener) {
+ mUsbManager = manager;
+ mContext = context;
+ mDeviceCallback = deviceListener;
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+ mHandler = new UsbDeviceResolverHandler(mHandlerThread.getLooper());
+ mPackageManager = context.getPackageManager();
+ }
+
+ /**
+ * Releases current object.
+ */
+ public void release() {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ }
+ }
+
+ /**
+ * Resolves handlers for USB device.
+ */
+ public void resolve(UsbDevice device) {
+ mHandler.requestResolveHandlers(device);
+ }
+
+ /**
+ * Dispatches device to component.
+ */
+ public boolean dispatch(UsbDevice device, ComponentName component, boolean inAoap) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "dispatch: " + device + " component: " + component + " inAoap: " + inAoap);
+ }
+
+ ActivityInfo activityInfo;
+ try {
+ activityInfo = mPackageManager.getActivityInfo(component, PackageManager.GET_META_DATA);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Activity not found: " + component);
+ return false;
+ }
+
+ Intent intent = createDeviceAttachedIntent(device);
+ if (inAoap) {
+ if (AoapInterface.isDeviceInAoapMode(device)) {
+ mDeviceCallback.onDeviceDispatched();
+ } else {
+ DeviceFilter filter =
+ packageMatches(activityInfo, intent.getAction(), device, true);
+ if (filter != null) {
+ requestAoapSwitch(device, filter);
+ return true;
+ }
+ }
+ }
+
+ intent.setComponent(component);
+ mUsbManager.grantPermission(device, activityInfo.applicationInfo.uid);
+
+ mContext.startActivity(intent);
+ mHandler.requestCompleteDeviceDispatch();
+ return true;
+ }
+
+ private static Intent createDeviceAttachedIntent(UsbDevice device) {
+ Intent intent = new Intent(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ intent.putExtra(UsbManager.EXTRA_DEVICE, device);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return intent;
+ }
+
+ private void doHandleResolveHandlers(UsbDevice device) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "doHandleResolveHandlers: " + device);
+ }
+
+ Intent intent = createDeviceAttachedIntent(device);
+ List<Pair<ResolveInfo, DeviceFilter>> matches = getDeviceMatches(device, intent, false);
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "matches size: " + matches.size());
+ }
+ List<UsbDeviceSettings> settings = new ArrayList<>(matches.size());
+ for (Pair<ResolveInfo, DeviceFilter> info : matches) {
+ UsbDeviceSettings setting = UsbDeviceSettings.constructSettings(device);
+ setting.setHandler(
+ new ComponentName(
+ info.first.activityInfo.packageName, info.first.activityInfo.name));
+ settings.add(setting);
+ }
+ DeviceContext deviceContext =
+ new DeviceContext(device, UsbDeviceSettings.constructSettings(device), settings);
+ if (AoapInterface.isSupported(deviceContext.connection)) {
+ deviceContext.mActiveDeviceOptions.addAll(getDeviceMatches(device, intent, true));
+ queryNextAoapHandler(deviceContext);
+ } else {
+ deviceProbingComplete(deviceContext);
+ }
+ }
+
+ private void queryNextAoapHandler(DeviceContext context) {
+ Pair<ResolveInfo, DeviceFilter> option = context.mActiveDeviceOptions.peek();
+ if (option == null) {
+ Log.w(TAG, "No more options left.");
+ deviceProbingComplete(context);
+ return;
+ }
+ Intent serviceIntent = new Intent();
+ serviceIntent.setComponent(ComponentName.unflattenFromString(option.second.mAoapService));
+ boolean bound = mContext.bindService(serviceIntent, context.mServiceConnection,
+ Context.BIND_AUTO_CREATE);
+ if (bound) {
+ mHandler.requestServiceConnectionTimeout();
+ } else {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "Failed to bind to the service");
+ }
+ context.mActiveDeviceOptions.poll();
+ queryNextAoapHandler(context);
+ }
+ }
+
+ private void requestAoapSwitch(UsbDevice device, DeviceFilter filter) {
+ UsbDeviceConnection connection = UsbUtil.openConnection(mUsbManager, device);
+ try {
+ UsbUtil.sendAoapAccessoryStart(
+ connection,
+ filter.mAoapManufacturer,
+ filter.mAoapModel,
+ filter.mAoapDescription,
+ filter.mAoapVersion,
+ filter.mAoapUri,
+ filter.mAoapSerial);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to switch device into AOAP mode", e);
+ }
+ connection.close();
+ }
+
+ private void deviceProbingComplete(DeviceContext context) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "deviceProbingComplete");
+ }
+ mDeviceCallback.onHandlersResolveCompleted(context.usbDevice, context.activeDeviceSettings);
+ }
+
+ private void doHandleServiceConnectionStateChanged(DeviceContext context) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "doHandleServiceConnectionStateChanged: "
+ + context.mUsbAoapSupportCheckService);
+ }
+ if (context.mUsbAoapSupportCheckService != null) {
+ boolean deviceSupported = false;
+ try {
+ deviceSupported =
+ context.mUsbAoapSupportCheckService.isDeviceSupported(context.usbDevice);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Call to remote service failed", e);
+ }
+ if (deviceSupported) {
+ Pair<ResolveInfo, DeviceFilter> option = context.mActiveDeviceOptions.peek();
+
+ UsbDeviceSettings setting = UsbDeviceSettings.constructSettings(context.settings);
+ setting.setHandler(
+ new ComponentName(
+ option.first.activityInfo.packageName, option.first.activityInfo.name));
+ setting.setAoap(true);
+ context.activeDeviceSettings.add(setting);
+ }
+ mContext.unbindService(context.mServiceConnection);
+ }
+ context.mActiveDeviceOptions.poll();
+ queryNextAoapHandler(context);
+ }
+
+ private List<Pair<ResolveInfo, DeviceFilter>> getDeviceMatches(
+ UsbDevice device, Intent intent, boolean forAoap) {
+ List<Pair<ResolveInfo, DeviceFilter>> matches = new ArrayList<>();
+ List<ResolveInfo> resolveInfos =
+ mPackageManager.queryIntentActivities(intent, PackageManager.GET_META_DATA);
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ DeviceFilter filter = packageMatches(resolveInfo.activityInfo,
+ intent.getAction(), device, forAoap);
+ if (filter != null) {
+ matches.add(Pair.create(resolveInfo, filter));
+ }
+ }
+ return matches;
+ }
+
+ private DeviceFilter packageMatches(ActivityInfo ai, String metaDataName, UsbDevice device,
+ boolean forAoap) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "packageMatches ai: " + ai + "metaDataName: " + metaDataName + " forAoap: "
+ + forAoap);
+ }
+ String filterTagName = forAoap ? "usb-aoap-accessory" : "usb-device";
+ XmlResourceParser parser = null;
+ try {
+ parser = ai.loadXmlMetaData(mPackageManager, metaDataName);
+ if (parser == null) {
+ Log.w(TAG, "no meta-data for " + ai);
+ return null;
+ }
+
+ XmlUtils.nextElement(parser);
+ while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ String tagName = parser.getName();
+ if (device != null && filterTagName.equals(tagName)) {
+ DeviceFilter filter = DeviceFilter.read(parser, forAoap);
+ if (forAoap || filter.matches(device)) {
+ return filter;
+ }
+ }
+ XmlUtils.nextElement(parser);
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to load component info " + ai.toString(), e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ return null;
+ }
+
+ private class UsbDeviceResolverHandler extends Handler {
+ private static final int MSG_RESOLVE_HANDLERS = 0;
+ private static final int MSG_SERVICE_CONNECTION_STATE_CHANGE = 1;
+ private static final int MSG_SERVICE_CONNECTION_TIMEOUT = 2;
+ private static final int MSG_COMPLETE_DISPATCH = 3;
+
+ private static final long CONNECT_TIMEOUT_MS = 5000;
+
+ private UsbDeviceResolverHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void requestResolveHandlers(UsbDevice device) {
+ Message msg = obtainMessage(MSG_RESOLVE_HANDLERS, device);
+ sendMessage(msg);
+ }
+
+ public void requestOnServiceConnectionStateChanged(DeviceContext deviceContext) {
+ sendMessage(obtainMessage(MSG_SERVICE_CONNECTION_STATE_CHANGE, deviceContext));
+ }
+
+ public void requestServiceConnectionTimeout() {
+ sendEmptyMessageDelayed(MSG_SERVICE_CONNECTION_TIMEOUT, CONNECT_TIMEOUT_MS);
+ }
+
+ public void requestCompleteDeviceDispatch() {
+ sendEmptyMessage(MSG_COMPLETE_DISPATCH);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_RESOLVE_HANDLERS:
+ doHandleResolveHandlers((UsbDevice) msg.obj);
+ break;
+ case MSG_SERVICE_CONNECTION_STATE_CHANGE:
+ removeMessages(MSG_SERVICE_CONNECTION_TIMEOUT);
+ doHandleServiceConnectionStateChanged((DeviceContext) msg.obj);
+ break;
+ case MSG_SERVICE_CONNECTION_TIMEOUT:
+ Log.i(TAG, "Service connection timeout");
+ doHandleServiceConnectionStateChanged(null);
+ break;
+ case MSG_COMPLETE_DISPATCH:
+ mDeviceCallback.onDeviceDispatched();
+ break;
+ default:
+ Log.w(TAG, "Unsupported message: " + msg);
+ }
+ }
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbDevicePreference.java b/car-usb-handler/src/android/car/usb/handler/UsbDevicePreference.java
new file mode 100644
index 0000000000..e4b6a8461f
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbDevicePreference.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.car.usb.handler;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.preference.Preference;
+
+/**
+ * Setting preference used for USB devices.
+ */
+public final class UsbDevicePreference extends Preference
+ implements Preference.OnPreferenceClickListener {
+
+ /**
+ * Callbacks to handle preference changes.
+ */
+ public interface UsbDevicePreferenceCallback {
+ /** Preference deleted */
+ void onUsbDevicePreferenceDelete(Preference preference, UsbDeviceSettings settings);
+ }
+
+ private final UsbDeviceSettings mUsbDeviceSettings;
+ private final UsbDevicePreferenceCallback mCallback;
+
+ public UsbDevicePreference(Context context, UsbDeviceSettings usbDeviceSettings,
+ UsbDevicePreferenceCallback callback) {
+ super(context);
+ mCallback = callback;
+ mUsbDeviceSettings = usbDeviceSettings;
+ setTitle(usbDeviceSettings.getDeviceName());
+ if (usbDeviceSettings.getHandler() != null) {
+ setSummary(usbDeviceSettings.getHandler().flattenToShortString());
+ }
+ setOnPreferenceClickListener(this);
+ }
+
+ @Override
+ public boolean onPreferenceClick(final Preference preference) {
+ new AlertDialog.Builder(getContext())
+ .setTitle(R.string.usb_pref_delete_title)
+ .setMessage(String.format(
+ getContext().getResources().getString(R.string.usb_pref_delete_message),
+ mUsbDeviceSettings.getDeviceName()))
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(R.string.usb_pref_delete_yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ mCallback.onUsbDevicePreferenceDelete(
+ preference, mUsbDeviceSettings);
+ }})
+ .setNegativeButton(R.string.usb_pref_delete_cancel, null)
+ .show();
+ return true;
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbDeviceSettings.java b/car-usb-handler/src/android/car/usb/handler/UsbDeviceSettings.java
new file mode 100644
index 0000000000..5084414093
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbDeviceSettings.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.car.usb.handler;
+
+import android.content.ComponentName;
+import android.hardware.usb.UsbDevice;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Settings for USB device.
+ * @hide
+ */
+public final class UsbDeviceSettings {
+
+ private final String mSerialNumber;
+ private final int mVid;
+ private final int mPid;
+ private String mDeviceName;
+ private ComponentName mHandler;
+ private boolean mAoap;
+ private boolean mDefaultHandler;
+
+ UsbDeviceSettings(String serialNumber, int vid, int pid) {
+ Preconditions.checkNotNull(serialNumber);
+
+ mSerialNumber = serialNumber;
+ mVid = vid;
+ mPid = pid;
+ }
+
+ public String getSerialNumber() {
+ return mSerialNumber;
+ }
+
+ public int getVid() {
+ return mVid;
+ }
+
+ public int getPid() {
+ return mPid;
+ }
+
+ public void setDeviceName(String deviceName) {
+ mDeviceName = deviceName;
+ }
+
+ public String getDeviceName() {
+ return mDeviceName;
+ }
+
+ public void setHandler(ComponentName handler) {
+ mHandler = handler;
+ }
+
+ public ComponentName getHandler() {
+ return mHandler;
+ }
+
+ public void setAoap(boolean aoap) {
+ mAoap = aoap;
+ }
+
+ public boolean getAoap() {
+ return mAoap;
+ }
+
+ public void setDefaultHandler(boolean defaultHandler) {
+ mDefaultHandler = defaultHandler;
+ }
+
+ public boolean isDefaultHandler() {
+ return mDefaultHandler;
+ }
+
+ @Override
+ public String toString() {
+ return "UsbDeviceSettings{serial=" + mSerialNumber + ", vid=" + mVid + ", pid=" + mPid
+ + ", name=" + mDeviceName + ", handler=" + mHandler.toString() + ", aoap=" + mAoap
+ + ", default=" + mDefaultHandler + "}";
+ }
+
+ /**
+ * Checks if setting matches {@code UsbDevice}.
+ */
+ public boolean matchesDevice(UsbDevice device) {
+ return getSerialNumber().equals(device.getSerialNumber());
+ }
+
+ /**
+ * Creates settings from {@code UsbDevice}.
+ */
+ public static UsbDeviceSettings constructSettings(UsbDevice device) {
+ UsbDeviceSettings settings = new UsbDeviceSettings(
+ device.getSerialNumber(), device.getVendorId(), device.getProductId());
+ settings.setDeviceName(device.getProductName());
+ return settings;
+ }
+
+ /**
+ * Creates settings from other settings.
+ * <p>
+ * Only basic properties are inherited.
+ */
+ public static UsbDeviceSettings constructSettings(UsbDeviceSettings origSettings) {
+ UsbDeviceSettings settings = new UsbDeviceSettings(
+ origSettings.getSerialNumber(), origSettings.getVid(), origSettings.getPid());
+ settings.setDeviceName(origSettings.getDeviceName());
+ return settings;
+ }
+
+ /**
+ * Creates settings.
+ */
+ public static UsbDeviceSettings constructSettings(String serialNumber, int vid, int pid,
+ String deviceName, ComponentName handler, boolean aoap) {
+ UsbDeviceSettings settings = new UsbDeviceSettings(serialNumber, vid, pid);
+ settings.setDeviceName(deviceName);
+ settings.setHandler(handler);
+ settings.setAoap(aoap);
+ return settings;
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbHostController.java b/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
new file mode 100644
index 0000000000..b705928fec
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbHostController.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.car.usb.handler;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Controller used to handle USB device connections.
+ * TODO: Support handling multiple new USB devices at the same time.
+ */
+public final class UsbHostController
+ implements UsbDeviceHandlerResolver.UsbDeviceHandlerResolverCallback {
+
+ /**
+ * Callbacks for controller
+ */
+ public interface UsbHostControllerCallbacks {
+ /** Host controller ready for shutdown */
+ void shutdown();
+ /** Change of processing state */
+ void processingStateChanged(boolean processing);
+ /** Title of processing changed */
+ void titleChanged(String title);
+ /** Options for USB device changed */
+ void optionsUpdated(List<UsbDeviceSettings> options);
+ }
+
+ private static final String TAG = UsbHostController.class.getSimpleName();
+ private static final boolean LOCAL_LOGD = true;
+ private static final boolean LOCAL_LOGV = true;
+
+
+ private final List<UsbDeviceSettings> mEmptyList = new ArrayList<>();
+ private final Context mContext;
+ private final UsbHostControllerCallbacks mCallback;
+ private final UsbSettingsStorage mUsbSettingsStorage;
+ private final UsbManager mUsbManager;
+ private final UsbDeviceHandlerResolver mUsbResolver;
+ private final UsbHostControllerHandler mHandler;
+
+ private final BroadcastReceiver mUsbBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
+ UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ unsetActiveDeviceIfSerialMatch(device);
+ } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
+ UsbDevice device = intent.<UsbDevice>getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ setActiveDeviceIfSerialMatch(device);
+ }
+ }
+ };
+
+ @GuardedBy("this")
+ private UsbDevice mActiveDevice;
+
+ @GuardedBy("this")
+ private String mProcessingDeviceSerial;
+
+ public UsbHostController(Context context, UsbHostControllerCallbacks callbacks) {
+ mContext = context;
+ mCallback = callbacks;
+ mHandler = new UsbHostControllerHandler(Looper.myLooper());
+ mUsbSettingsStorage = new UsbSettingsStorage(context);
+ mUsbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
+ mUsbResolver = new UsbDeviceHandlerResolver(mUsbManager, mContext, this);
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ context.registerReceiver(mUsbBroadcastReceiver, filter);
+
+ }
+
+ private synchronized void setActiveDeviceIfSerialMatch(UsbDevice device) {
+ if (device != null && device.getSerialNumber() != null
+ && device.getSerialNumber().equals(mProcessingDeviceSerial)) {
+ mActiveDevice = device;
+ }
+ }
+
+ private synchronized void unsetActiveDeviceIfSerialMatch(UsbDevice device) {
+ mHandler.requestDeviceRemoved();
+ if (mActiveDevice != null && mActiveDevice.getSerialNumber() != null
+ && mActiveDevice.getSerialNumber().equals(device.getSerialNumber())) {
+ mActiveDevice = null;
+ }
+ }
+
+ private synchronized boolean startDeviceProcessingIfNull(UsbDevice device) {
+ if (mActiveDevice == null) {
+ mActiveDevice = device;
+ mProcessingDeviceSerial = device.getSerialNumber();
+ return true;
+ }
+ return false;
+ }
+
+ private synchronized void stopDeviceProcessing() {
+ mActiveDevice = null;
+ mProcessingDeviceSerial = null;
+ }
+
+ private synchronized UsbDevice getActiveDevice() {
+ return mActiveDevice;
+ }
+
+ private boolean deviceMatchedActiveDevice(UsbDevice device) {
+ UsbDevice activeDevice = getActiveDevice();
+ return activeDevice != null && activeDevice.getSerialNumber() != null
+ && activeDevice.getSerialNumber().equals(device.getSerialNumber());
+ }
+
+ /**
+ * Processes device new device.
+ * <p>
+ * It will load existing settings or resolve supported handlers.
+ */
+ public void processDevice(UsbDevice device) {
+ if (!startDeviceProcessingIfNull(device)) {
+ Log.w(TAG, "Currently, other device is being processed");
+ }
+ mCallback.optionsUpdated(mEmptyList);
+ mCallback.processingStateChanged(true);
+
+ UsbDeviceSettings settings = mUsbSettingsStorage.getSettings(device.getSerialNumber());
+ if (settings != null && mUsbResolver.dispatch(
+ mActiveDevice, settings.getHandler(), settings.getAoap())) {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "Usb Device: " + device + " was sent to component: "
+ + settings.getHandler());
+ }
+ return;
+ }
+ mCallback.titleChanged(device.getManufacturerName() + " " + device.getProductName());
+ mUsbResolver.resolve(device);
+ }
+
+ /**
+ * Applies device settings.
+ */
+ public void applyDeviceSettings(UsbDeviceSettings settings) {
+ mUsbSettingsStorage.saveSettings(settings);
+ mUsbResolver.dispatch(getActiveDevice(), settings.getHandler(), settings.getAoap());
+ }
+
+ /**
+ * Release object.
+ */
+ public void release() {
+ mContext.unregisterReceiver(mUsbBroadcastReceiver);
+ mUsbResolver.release();
+ }
+
+ @Override
+ public void onHandlersResolveCompleted(
+ UsbDevice device, List<UsbDeviceSettings> handlers) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "onHandlersResolveComplete: " + device);
+ }
+ if (deviceMatchedActiveDevice(device)) {
+ mCallback.processingStateChanged(false);
+ if (handlers.isEmpty()) {
+ onDeviceDispatched();
+ } else if (handlers.size() == 1) {
+ applyDeviceSettings(handlers.get(0));
+ } else {
+ mCallback.optionsUpdated(handlers);
+ }
+ } else {
+ Log.w(TAG, "Handlers ignored as they came for inactive device");
+ }
+ }
+
+ @Override
+ public void onDeviceDispatched() {
+ stopDeviceProcessing();
+ mCallback.shutdown();
+ }
+
+ void doHandleDeviceRemoved() {
+ if (getActiveDevice() == null) {
+ if (LOCAL_LOGD) {
+ Log.d(TAG, "USB device detached");
+ }
+ stopDeviceProcessing();
+ mCallback.shutdown();
+ }
+ }
+
+ private class UsbHostControllerHandler extends Handler {
+ private static final int MSG_DEVICE_REMOVED = 1;
+
+ private static final int DEVICE_REMOVE_TIMEOUT_MS = 500;
+
+ private UsbHostControllerHandler(Looper looper) {
+ super(looper);
+ }
+
+ private void requestDeviceRemoved() {
+ sendEmptyMessageDelayed(MSG_DEVICE_REMOVED, DEVICE_REMOVE_TIMEOUT_MS);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DEVICE_REMOVED:
+ doHandleDeviceRemoved();
+ break;
+ default:
+ Log.w(TAG, "Unhandled message: " + msg);
+ super.handleMessage(msg);
+ }
+ }
+ }
+
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java b/car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java
new file mode 100644
index 0000000000..387ae62cd3
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbHostManagementActivity.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.car.usb.handler;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import java.util.List;
+
+/**
+ * Activity to handle USB device attached.
+ * <p>
+ * When user plugs in USB device: a) Device was used before and user selected handler for it. In
+ * this case handler will be launched. b) Device has not handler assigned. In this case supported
+ * handlers will be captured, and user will be presented with choice to assign default handler.
+ * After that handler will be launched.
+ */
+public class UsbHostManagementActivity extends Activity
+ implements UsbHostController.UsbHostControllerCallbacks {
+ private static final String TAG = UsbHostManagementActivity.class.getSimpleName();
+
+ private HandlersAdapter mListAdapter;
+ private ListView mHandlersList;
+ private LinearLayout mProgressInfo;
+ private UsbHostController mController;
+ private PackageManager mPackageManager;
+
+ private final AdapterView.OnItemClickListener mHandlerClickListener =
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> parent, final View view, int position, long id) {
+ UsbDeviceSettings settings = (UsbDeviceSettings) parent.getItemAtPosition(position);
+ settings.setDefaultHandler(true);
+ mController.applyDeviceSettings(settings);
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.usb_host);
+ mHandlersList = (ListView) findViewById(R.id.usb_handlers_list);
+ mProgressInfo = (LinearLayout) findViewById(R.id.usb_handlers_progress);
+ mListAdapter = new HandlersAdapter(this);
+ mHandlersList.setAdapter(mListAdapter);
+ mHandlersList.setOnItemClickListener(mHandlerClickListener);
+ mController = new UsbHostController(this, this);
+ mPackageManager = getPackageManager();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mController.release();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ UsbDevice connectedDevice = getDevice();
+ if (connectedDevice != null) {
+ mController.processDevice(connectedDevice);
+ } else {
+ finish();
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void processingStateChanged(final boolean processing) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mProgressInfo.setVisibility(processing ? View.VISIBLE : View.GONE);
+ }
+ });
+ }
+
+ @Override
+ public void titleChanged(final String title) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ setTitle(title);
+ }
+ });
+ }
+
+ @Override
+ public void optionsUpdated(final List<UsbDeviceSettings> options) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mListAdapter.clear();
+ mListAdapter.addAll(options);
+ }
+ });
+ }
+
+
+ @Override
+ public void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ }
+
+ @Nullable
+ private UsbDevice getDevice() {
+ if (!UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(getIntent().getAction())) {
+ return null;
+ }
+ return (UsbDevice) getIntent().getParcelableExtra(UsbManager.EXTRA_DEVICE);
+ }
+
+ private class HandlersAdapter extends ArrayAdapter<UsbDeviceSettings> {
+ class HandlerHolder {
+ public TextView mAppName;
+ public ImageView mAppIcon;
+ }
+
+ HandlersAdapter(Context context) {
+ super(context, R.layout.usb_handler_row);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View rowView = convertView;
+ if (rowView == null) {
+ rowView = getLayoutInflater().inflate(R.layout.usb_handler_row, null);
+ HandlerHolder holder = new HandlerHolder();
+ holder.mAppName = (TextView) rowView.findViewById(R.id.usb_handler_title);
+ holder.mAppIcon = (ImageView) rowView.findViewById(R.id.usb_handler_icon);
+ rowView.setTag(holder);
+ }
+
+ HandlerHolder holder = (HandlerHolder) rowView.getTag();
+ ComponentName handler = getItem(position).getHandler();
+
+ try {
+ ApplicationInfo appInfo =
+ mPackageManager.getApplicationInfo(handler.getPackageName(), 0);
+ holder.mAppName.setText(appInfo.loadLabel(mPackageManager));
+ holder.mAppIcon.setImageDrawable(appInfo.loadIcon(mPackageManager));
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Handling package not found: " + handler.getPackageName());
+ holder.mAppName.setText(handler.flattenToShortString());
+ holder.mAppIcon.setImageResource(android.R.color.transparent);
+ }
+ return rowView;
+ }
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java b/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
new file mode 100644
index 0000000000..157c92f9d6
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbSettingsStorage.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.car.usb.handler;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides API to persist USB device settings.
+ */
+public final class UsbSettingsStorage {
+ private static final String TAG = UsbSettingsStorage.class.getSimpleName();
+
+ private static final String TABLE_USB_SETTINGS = "usb_devices";
+ private static final String COLUMN_SERIAL = "serial";
+ private static final String COLUMN_VID = "vid";
+ private static final String COLUMN_PID = "pid";
+ private static final String COLUMN_NAME = "name";
+ private static final String COLUMN_HANDLER = "handler";
+ private static final String COLUMN_AOAP = "aoap";
+ private static final String COLUMN_DEFAULT_HANDLER = "default_handler";
+
+ private final UsbSettingsDbHelper mDbHelper;
+
+ public UsbSettingsStorage(Context context) {
+ mDbHelper = new UsbSettingsDbHelper(context);
+ }
+
+ /**
+ * Returns settings for {@serialNumber} or null if it doesn't exist.
+ */
+ @Nullable
+ public UsbDeviceSettings getSettings(String serialNumber) {
+ try (SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ Cursor resultCursor = db.query(
+ TABLE_USB_SETTINGS,
+ null,
+ COLUMN_SERIAL + " = ?",
+ new String[]{serialNumber},
+ null,
+ null,
+ null)) {
+ if (resultCursor.getCount() > 1) {
+ throw new RuntimeException("Querying for serial number: " + serialNumber
+ + " returned " + resultCursor.getCount() + " results");
+ }
+ if (resultCursor.getCount() == 0) {
+ Log.w(TAG, "Usb setting missing for device serial: " + serialNumber);
+ return null;
+ }
+ List<UsbDeviceSettings> settings = constructSettings(resultCursor);
+ return settings.get(0);
+ }
+ }
+
+ /**
+ * Saves or updates settings for USB device.
+ */
+ public void saveSettings(UsbDeviceSettings settings) {
+ try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+ long result = db.replace(
+ TABLE_USB_SETTINGS,
+ null,
+ settingsToContentValues(settings));
+ if (result == -1) {
+ Log.e(TAG, "Failed to save settings: " + settings);
+ }
+ }
+ }
+
+ /**
+ * Delete settings for USB device.
+ */
+ public void deleteSettings(String serialNumber, int vid, int pid) {
+ try (SQLiteDatabase db = mDbHelper.getWritableDatabase()) {
+ int result = db.delete(
+ TABLE_USB_SETTINGS,
+ COLUMN_SERIAL + " = ? AND " + COLUMN_VID + " = ? AND " + COLUMN_PID
+ + " = ?",
+ new String[]{serialNumber, Integer.toString(vid), Integer.toString(pid)});
+ if (result == 0) {
+ Log.w(TAG, "No settings with serialNumber: " + serialNumber
+ + " vid: " + vid + " pid: " + pid);
+ }
+ if (result > 1) {
+ Log.e(TAG, "Deleted multiple rows (" + result + ") for serialNumber: "
+ + serialNumber + " vid: " + vid + " pid: " + pid);
+ }
+ }
+ }
+
+ /**
+ * Returns all saved settings.
+ */
+ public List<UsbDeviceSettings> getAllSettings() {
+ try (SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ Cursor resultCursor = db.query(
+ TABLE_USB_SETTINGS,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null)) {
+ return constructSettings(resultCursor);
+ }
+ }
+
+ private List<UsbDeviceSettings> constructSettings(Cursor cursor) {
+ if (!cursor.isBeforeFirst()) {
+ throw new RuntimeException("Cursor is not reset to before first element");
+ }
+ int serialNumberColumnId = cursor.getColumnIndex(COLUMN_SERIAL);
+ int vidColumnId = cursor.getColumnIndex(COLUMN_VID);
+ int pidColumnId = cursor.getColumnIndex(COLUMN_PID);
+ int deviceNameColumnId = cursor.getColumnIndex(COLUMN_NAME);
+ int handlerColumnId = cursor.getColumnIndex(COLUMN_HANDLER);
+ int aoapColumnId = cursor.getColumnIndex(COLUMN_AOAP);
+ List<UsbDeviceSettings> results = new ArrayList<>(cursor.getCount());
+ while (cursor.moveToNext()) {
+ results.add(UsbDeviceSettings.constructSettings(
+ cursor.getString(serialNumberColumnId),
+ cursor.getInt(vidColumnId),
+ cursor.getInt(pidColumnId),
+ cursor.getString(deviceNameColumnId),
+ ComponentName.unflattenFromString(
+ cursor.getString(handlerColumnId)),
+ cursor.getInt(aoapColumnId) != 0));
+ }
+ return results;
+ }
+
+ /**
+ * Converts {@code UsbDeviceSettings} to {@code ContentValues}.
+ */
+ public ContentValues settingsToContentValues(UsbDeviceSettings settings) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(COLUMN_SERIAL, settings.getSerialNumber());
+ contentValues.put(COLUMN_VID, settings.getVid());
+ contentValues.put(COLUMN_PID, settings.getPid());
+ contentValues.put(COLUMN_NAME, settings.getDeviceName());
+ contentValues.put(COLUMN_HANDLER, settings.getHandler().flattenToShortString());
+ contentValues.put(COLUMN_AOAP, settings.getAoap() ? 1 : 0);
+ contentValues.put(COLUMN_DEFAULT_HANDLER, settings.isDefaultHandler() ? 1 : 0);
+ return contentValues;
+ }
+
+
+ private static class UsbSettingsDbHelper extends SQLiteOpenHelper {
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "usb_devices.db";
+
+ UsbSettingsDbHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + TABLE_USB_SETTINGS + " ("
+ + COLUMN_SERIAL + " TEXT,"
+ + COLUMN_VID + " INTEGER,"
+ + COLUMN_PID + " INTEGER,"
+ + COLUMN_NAME + " TEXT, "
+ + COLUMN_HANDLER + " TEXT,"
+ + COLUMN_AOAP + " INTEGER,"
+ + COLUMN_DEFAULT_HANDLER + " INTEGER," + "PRIMARY KEY (" + COLUMN_SERIAL
+ + "))");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ // Do nothing at this point. Not required for v1 database.
+ }
+ }
+}
diff --git a/car-usb-handler/src/android/car/usb/handler/UsbUtil.java b/car-usb-handler/src/android/car/usb/handler/UsbUtil.java
new file mode 100644
index 0000000000..939fe3fa09
--- /dev/null
+++ b/car-usb-handler/src/android/car/usb/handler/UsbUtil.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * 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 android.car.usb.handler;
+
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbDeviceConnection;
+import android.hardware.usb.UsbManager;
+import android.text.TextUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Util methods to work with USB devices.
+ */
+class UsbUtil {
+ public static List<UsbDevice> findAllPossibleAndroidDevices(UsbManager usbManager) {
+ HashMap<String, UsbDevice> devices = usbManager.getDeviceList();
+ ArrayList<UsbDevice> androidDevices = new ArrayList<>(devices.size());
+ for (UsbDevice device : devices.values()) {
+ UsbDeviceConnection connection = openConnection(usbManager, device);
+ if (AoapInterface.isSupported(connection)) {
+ androidDevices.add(device);
+ }
+ connection.close();
+ }
+ return androidDevices;
+ }
+
+ public static UsbDeviceConnection openConnection(UsbManager manager, UsbDevice device) {
+ manager.grantPermission(device);
+ return manager.openDevice(device);
+ }
+
+ public static void sendAoapAccessoryStart(UsbDeviceConnection connection, String manufacturer,
+ String model, String description, String version, String uri, String serial)
+ throws IOException {
+ AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_MANUFACTURER,
+ manufacturer);
+ AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_MODEL,
+ model);
+ AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_DESCRIPTION,
+ description);
+ AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_VERSION,
+ version);
+ AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_URI, uri);
+ AoapInterface.sendString(connection, AoapInterface.ACCESSORY_STRING_SERIAL,serial);
+ AoapInterface.sendAoapStart(connection);
+ }
+
+ public static boolean isTheSameDevice(UsbDevice l, UsbDevice r) {
+ if (TextUtils.equals(l.getManufacturerName(), r.getManufacturerName())
+ && TextUtils.equals(l.getProductName(), r.getProductName())
+ && TextUtils.equals(l.getSerialNumber(), r.getSerialNumber())) {
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isDevicesMatching(UsbDevice l, UsbDevice r) {
+ if (l.getVendorId() == r.getVendorId() && l.getProductId() == r.getProductId()
+ && TextUtils.equals(l.getSerialNumber(), r.getSerialNumber())) {
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean isDeviceConnected(UsbManager usbManager, UsbDevice device) {
+ HashMap<String, UsbDevice> devices = usbManager.getDeviceList();
+ for (UsbDevice dev : devices.values()) {
+ if (isDevicesMatching(dev, device)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/car_product/build/car.mk b/car_product/build/car.mk
index fe9b644542..5928164906 100644
--- a/car_product/build/car.mk
+++ b/car_product/build/car.mk
@@ -91,6 +91,7 @@ PRODUCT_PACKAGES += \
Stream \
CarHvacApp \
CarMapsPlaceholder \
+ CarUsbHandler \
android.car \
libvehiclenetwork-native \
libvehiclemonitor-native \