blob: aed59a2962ee1b4613132736b8317e7f8b318683 [file] [log] [blame]
/*
* 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 static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.Manifest;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
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.UsbManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import com.android.internal.util.XmlUtils;
import org.xmlpull.v1.XmlPullParser;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
/** 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;
private static final String AOAP_HANDLE_PERMISSION =
"android.car.permission.CAR_HANDLE_USB_AOAP_DEVICE";
/**
* 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 AoapServiceManager mAoapServiceManager;
private HandlerThread mHandlerThread;
private UsbDeviceResolverHandler mUsbDeviceResolverHandler;
public UsbDeviceHandlerResolver(UsbManager manager, Context context,
UsbDeviceHandlerResolverCallback deviceListener) {
mUsbManager = manager;
mContext = context;
mDeviceCallback = deviceListener;
createHandlerThread();
mPackageManager = context.getPackageManager();
mAoapServiceManager = new AoapServiceManager(mContext.getApplicationContext());
}
/**
* Releases current object.
*/
public void release() {
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
}
}
/**
* Resolves handlers for USB device.
*/
public void resolve(UsbDevice device) {
mUsbDeviceResolverHandler.requestResolveHandlers(device);
}
/**
* Listener for failed {@code startAosp} command.
*
* <p>If {@code startAosp} fails, the device could be left in a inconsistent state, that's why
* we go back to USB enumeration, instead of just repeating the command.
*/
public interface StartAoapFailureListener {
/** Called if startAoap fails. */
void onFailure(UsbDevice failedDevice);
}
/**
* Dispatches device to component.
*/
public boolean dispatch(UsbDevice device, ComponentName component, boolean inAoap,
StartAoapFailureListener failureListener) {
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 {
UsbDeviceFilter filter =
packageMatches(activityInfo, intent.getAction(), device, true);
if (filter != null) {
if (!mHandlerThread.isAlive()) {
// Start a new thread. Used only when startAoap fails, and we need to
// re-enumerate device in order to try again.
createHandlerThread();
}
mUsbDeviceResolverHandler.post(() -> {
if (mAoapServiceManager.canSwitchDeviceToAoap(device,
ComponentName.unflattenFromString(filter.mAoapService))) {
try {
requestAoapSwitch(device, filter);
} catch (IOException e) {
Log.w(TAG, "Start AOAP command failed:" + e);
failureListener.onFailure(device);
}
} else {
Log.i(TAG, "Ignore AOAP switch for device " + device
+ " handled by " + filter.mAoapService);
}
});
mDeviceCallback.onDeviceDispatched();
return true;
}
}
}
intent.setComponent(component);
mUsbManager.grantPermission(device, activityInfo.applicationInfo.uid);
mContext.startActivity(intent);
mUsbDeviceResolverHandler.requestCompleteDeviceDispatch();
return true;
}
private void createHandlerThread() {
mHandlerThread = new HandlerThread(TAG);
mHandlerThread.start();
mUsbDeviceResolverHandler = new UsbDeviceResolverHandler(mHandlerThread.getLooper());
}
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<UsbHandlerPackage> matches = getDeviceMatches(device, intent, false);
if (LOCAL_LOGD) {
Log.d(TAG, "matches size: " + matches.size());
}
List<UsbDeviceSettings> settings = new ArrayList<>();
for (UsbHandlerPackage pkg : matches) {
settings.add(createSettings(device, pkg));
}
UsbDeviceConnection devConnection = UsbUtil.openConnection(mUsbManager, device);
if (devConnection != null && AoapInterface.isSupported(mContext, device, devConnection)) {
for (UsbHandlerPackage pkg : getDeviceMatches(device, intent, true)) {
if (mAoapServiceManager.isDeviceSupported(device, pkg.mAoapService)) {
settings.add(createSettings(device, pkg));
}
}
}
deviceProbingComplete(device, settings);
}
private UsbDeviceSettings createSettings(UsbDevice device, UsbHandlerPackage pkg) {
UsbDeviceSettings settings = UsbDeviceSettings.constructSettings(device);
settings.setHandler(pkg.mActivity);
settings.setAoap(pkg.mAoapService != null);
return settings;
}
private void requestAoapSwitch(UsbDevice device, UsbDeviceFilter filter) throws IOException {
UsbDeviceConnection connection = UsbUtil.openConnection(mUsbManager, device);
if (connection == null) {
Log.e(TAG, "Failed to connect to usb device.");
return;
}
try {
String hashedSerial = getHashed(Build.getSerial());
UsbUtil.sendAoapAccessoryStart(
connection,
filter.mAoapManufacturer,
filter.mAoapModel,
filter.mAoapDescription,
filter.mAoapVersion,
filter.mAoapUri,
hashedSerial);
} finally {
connection.close();
}
}
private String getHashed(String serial) {
try {
byte[] digest = MessageDigest.getInstance("MD5").digest(serial.getBytes());
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
Log.w(TAG, "could not create MD5 for serial number: " + serial);
return Integer.toString(serial.hashCode());
}
}
private void deviceProbingComplete(UsbDevice device, List<UsbDeviceSettings> settings) {
if (LOCAL_LOGD) {
Log.d(TAG, "deviceProbingComplete");
}
mDeviceCallback.onHandlersResolveCompleted(device, settings);
}
private List<UsbHandlerPackage> getDeviceMatches(
UsbDevice device, Intent intent, boolean forAoap) {
List<UsbHandlerPackage> matches = new ArrayList<>();
List<ResolveInfo> resolveInfos =
mPackageManager.queryIntentActivities(intent, PackageManager.GET_META_DATA);
for (ResolveInfo resolveInfo : resolveInfos) {
final String packageName = resolveInfo.activityInfo.packageName;
if (forAoap && !hasAoapPermission(packageName)) {
Log.w(TAG, "Package " + packageName + " does not hold "
+ AOAP_HANDLE_PERMISSION + " permission. Ignore the package.");
continue;
}
UsbDeviceFilter filter = packageMatches(resolveInfo.activityInfo,
intent.getAction(), device, forAoap);
if (filter != null) {
ActivityInfo ai = resolveInfo.activityInfo;
ComponentName activity = new ComponentName(ai.packageName, ai.name);
ComponentName aoapService = filter.mAoapService == null
? null : ComponentName.unflattenFromString(filter.mAoapService);
if (aoapService != null && !checkServiceRequiresPermission(aoapService)) {
continue;
}
if (aoapService != null || !forAoap) {
matches.add(new UsbHandlerPackage(activity, aoapService));
}
}
}
return matches;
}
private boolean checkServiceRequiresPermission(ComponentName serviceName) {
Intent intent = new Intent();
intent.setComponent(serviceName);
boolean found = false;
for (ResolveInfo info : mPackageManager.queryIntentServices(intent, 0)) {
if (info.serviceInfo != null) {
found = true;
if ((Manifest.permission.MANAGE_USB.equals(info.serviceInfo.permission))) {
return true;
}
}
}
if (found) {
Log.w(TAG, "Component " + serviceName + " must be protected with "
+ Manifest.permission.MANAGE_USB + " permission");
} else {
Log.w(TAG, "Component " + serviceName + " not found");
}
return false;
}
private boolean hasAoapPermission(String packageName) {
return mPackageManager
.checkPermission(AOAP_HANDLE_PERMISSION, packageName) == PERMISSION_GRANTED;
}
private UsbDeviceFilter 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";
try (XmlResourceParser 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)) {
UsbDeviceFilter filter = UsbDeviceFilter.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);
}
return null;
}
private class UsbDeviceResolverHandler extends Handler {
private static final int MSG_RESOLVE_HANDLERS = 0;
private static final int MSG_COMPLETE_DISPATCH = 3;
private UsbDeviceResolverHandler(Looper looper) {
super(looper);
}
void requestResolveHandlers(UsbDevice device) {
Message msg = obtainMessage(MSG_RESOLVE_HANDLERS, device);
sendMessage(msg);
}
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_COMPLETE_DISPATCH:
mDeviceCallback.onDeviceDispatched();
break;
default:
Log.w(TAG, "Unsupported message: " + msg);
}
}
}
private static class UsbHandlerPackage {
final ComponentName mActivity;
final @Nullable ComponentName mAoapService;
UsbHandlerPackage(ComponentName activity, @Nullable ComponentName aoapService) {
mActivity = activity;
mAoapService = aoapService;
}
}
}