/* * Copyright (C) 2011 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 com.android.server.usb; import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; 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.UsbAccessory; import android.hardware.usb.UsbManager; import android.os.Binder; import android.os.FileUtils; import android.os.Process; import android.util.Log; import android.util.SparseBooleanArray; import android.util.Xml; import com.android.internal.content.PackageMonitor; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; class UsbDeviceSettingsManager { private static final String TAG = "UsbDeviceSettingsManager"; private static final File sSettingsFile = new File("/data/system/usb_device_manager.xml"); private final Context mContext; private final PackageManager mPackageManager; // Temporary mapping UsbAccessory to list of UIDs with permissions for the accessory private final HashMap mAccessoryPermissionMap = new HashMap(); // Maps AccessoryFilter to user preferred application package private final HashMap mAccessoryPreferenceMap = new HashMap(); private final Object mLock = new Object(); // This class is used to describe a USB accessory. // 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 AccessoryFilter { // USB accessory manufacturer (or null for unspecified) public final String mManufacturer; // USB accessory model (or null for unspecified) public final String mModel; // USB accessory version (or null for unspecified) public final String mVersion; public AccessoryFilter(String manufacturer, String model, String version) { mManufacturer = manufacturer; mModel = model; mVersion = version; } public AccessoryFilter(UsbAccessory accessory) { mManufacturer = accessory.getManufacturer(); mModel = accessory.getModel(); mVersion = accessory.getVersion(); } public static AccessoryFilter read(XmlPullParser parser) throws XmlPullParserException, IOException { String manufacturer = null; String model = null; String version = null; int count = parser.getAttributeCount(); for (int i = 0; i < count; i++) { String name = parser.getAttributeName(i); String value = parser.getAttributeValue(i); if ("manufacturer".equals(name)) { manufacturer = value; } else if ("model".equals(name)) { model = value; } else if ("version".equals(name)) { version = value; } } return new AccessoryFilter(manufacturer, model, version); } public void write(XmlSerializer serializer)throws IOException { serializer.startTag(null, "usb-accessory"); if (mManufacturer != null) { serializer.attribute(null, "manufacturer", mManufacturer); } if (mModel != null) { serializer.attribute(null, "model", mModel); } if (mVersion != null) { serializer.attribute(null, "version", mVersion); } serializer.endTag(null, "usb-accessory"); } public boolean matches(UsbAccessory acc) { if (mManufacturer != null && !acc.getManufacturer().equals(mManufacturer)) return false; if (mModel != null && !acc.getModel().equals(mModel)) return false; if (mVersion != null && !acc.getVersion().equals(mVersion)) return false; return true; } public boolean matches(AccessoryFilter f) { if (mManufacturer != null && !f.mManufacturer.equals(mManufacturer)) return false; if (mModel != null && !f.mModel.equals(mModel)) return false; if (mVersion != null && !f.mVersion.equals(mVersion)) return false; return true; } @Override public boolean equals(Object obj) { // can't compare if we have wildcard strings if (mManufacturer == null || mModel == null || mVersion == null) { return false; } if (obj instanceof AccessoryFilter) { AccessoryFilter filter = (AccessoryFilter)obj; return (mManufacturer.equals(filter.mManufacturer) && mModel.equals(filter.mModel) && mVersion.equals(filter.mVersion)); } if (obj instanceof UsbAccessory) { UsbAccessory accessory = (UsbAccessory)obj; return (mManufacturer.equals(accessory.getManufacturer()) && mModel.equals(accessory.getModel()) && mVersion.equals(accessory.getVersion())); } return false; } @Override public int hashCode() { return ((mManufacturer == null ? 0 : mManufacturer.hashCode()) ^ (mModel == null ? 0 : mModel.hashCode()) ^ (mVersion == null ? 0 : mVersion.hashCode())); } @Override public String toString() { return "AccessoryFilter[mManufacturer=\"" + mManufacturer + "\", mModel=\"" + mModel + "\", mVersion=\"" + mVersion + "\"]"; } } private class MyPackageMonitor extends PackageMonitor { public void onPackageAdded(String packageName, int uid) { handlePackageUpdate(packageName); } public void onPackageChanged(String packageName, int uid, String[] components) { handlePackageUpdate(packageName); } public void onPackageRemoved(String packageName, int uid) { clearDefaults(packageName); } } MyPackageMonitor mPackageMonitor = new MyPackageMonitor(); public UsbDeviceSettingsManager(Context context) { mContext = context; mPackageManager = context.getPackageManager(); synchronized (mLock) { readSettingsLocked(); } mPackageMonitor.register(context, true); } private void readPreference(XmlPullParser parser) throws XmlPullParserException, IOException { String packageName = null; int count = parser.getAttributeCount(); for (int i = 0; i < count; i++) { if ("package".equals(parser.getAttributeName(i))) { packageName = parser.getAttributeValue(i); break; } } XmlUtils.nextElement(parser); if ("usb-accessory".equals(parser.getName())) { AccessoryFilter filter = AccessoryFilter.read(parser); mAccessoryPreferenceMap.put(filter, packageName); } XmlUtils.nextElement(parser); } private void readSettingsLocked() { FileInputStream stream = null; try { stream = new FileInputStream(sSettingsFile); XmlPullParser parser = Xml.newPullParser(); parser.setInput(stream, null); XmlUtils.nextElement(parser); while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if ("preference".equals(tagName)) { readPreference(parser); } else { XmlUtils.nextElement(parser); } } } catch (FileNotFoundException e) { Log.w(TAG, "settings file not found"); } catch (Exception e) { Log.e(TAG, "error reading settings file, deleting to start fresh", e); sSettingsFile.delete(); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { } } } } private void writeSettingsLocked() { FileOutputStream fos = null; try { FileOutputStream fstr = new FileOutputStream(sSettingsFile); Log.d(TAG, "writing settings to " + fstr); BufferedOutputStream str = new BufferedOutputStream(fstr); FastXmlSerializer serializer = new FastXmlSerializer(); serializer.setOutput(str, "utf-8"); serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, "settings"); for (AccessoryFilter filter : mAccessoryPreferenceMap.keySet()) { serializer.startTag(null, "preference"); serializer.attribute(null, "package", mAccessoryPreferenceMap.get(filter)); filter.write(serializer); serializer.endTag(null, "preference"); } serializer.endTag(null, "settings"); serializer.endDocument(); str.flush(); FileUtils.sync(fstr); str.close(); } catch (Exception e) { Log.e(TAG, "error writing settings file, deleting to start fresh", e); sSettingsFile.delete(); } } // Checks to see if a package matches an accessory. private boolean packageMatchesLocked(ResolveInfo info, String metaDataName, UsbAccessory accessory) { ActivityInfo ai = info.activityInfo; XmlResourceParser parser = null; try { parser = ai.loadXmlMetaData(mPackageManager, metaDataName); if (parser == null) { Log.w(TAG, "no meta-data for " + info); return false; } XmlUtils.nextElement(parser); while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if (accessory != null && "usb-accessory".equals(tagName)) { AccessoryFilter filter = AccessoryFilter.read(parser); if (filter.matches(accessory)) { return true; } } XmlUtils.nextElement(parser); } } catch (Exception e) { Log.w(TAG, "Unable to load component info " + info.toString(), e); } finally { if (parser != null) parser.close(); } return false; } private final ArrayList getAccessoryMatchesLocked( UsbAccessory accessory, Intent intent) { ArrayList matches = new ArrayList(); List resolveInfos = mPackageManager.queryIntentActivities(intent, PackageManager.GET_META_DATA); int count = resolveInfos.size(); for (int i = 0; i < count; i++) { ResolveInfo resolveInfo = resolveInfos.get(i); if (packageMatchesLocked(resolveInfo, intent.getAction(), accessory)) { matches.add(resolveInfo); } } return matches; } public void accessoryAttached(UsbAccessory accessory) { Intent intent = new Intent(UsbManager.ACTION_USB_ACCESSORY_ATTACHED); intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ArrayList matches; String defaultPackage; synchronized (mLock) { matches = getAccessoryMatchesLocked(accessory, intent); // Launch our default activity directly, if we have one. // Otherwise we will start the UsbResolverActivity to allow the user to choose. defaultPackage = mAccessoryPreferenceMap.get(new AccessoryFilter(accessory)); } resolveActivity(intent, matches, defaultPackage, accessory); } public void accessoryDetached(UsbAccessory accessory) { // clear temporary permissions for the accessory mAccessoryPermissionMap.remove(accessory); Intent intent = new Intent( UsbManager.ACTION_USB_ACCESSORY_DETACHED); intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory); mContext.sendBroadcast(intent); } private void resolveActivity(Intent intent, ArrayList matches, String defaultPackage, UsbAccessory accessory) { int count = matches.size(); // don't show the resolver activity if there are no choices available if (count == 0) { if (accessory != null) { String uri = accessory.getUri(); if (uri != null && uri.length() > 0) { // display URI to user // start UsbResolverActivity so user can choose an activity Intent dialogIntent = new Intent(); dialogIntent.setClassName("com.android.systemui", "com.android.systemui.usb.UsbAccessoryUriActivity"); dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); dialogIntent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory); dialogIntent.putExtra("uri", uri); try { mContext.startActivity(dialogIntent); } catch (ActivityNotFoundException e) { Log.e(TAG, "unable to start UsbAccessoryUriActivity"); } } } // do nothing return; } ResolveInfo defaultRI = null; if (count == 1 && defaultPackage == null) { // Check to see if our single choice is on the system partition. // If so, treat it as our default without calling UsbResolverActivity ResolveInfo rInfo = matches.get(0); if (rInfo.activityInfo != null && rInfo.activityInfo.applicationInfo != null && (rInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { defaultRI = rInfo; } } if (defaultRI == null && defaultPackage != null) { // look for default activity for (int i = 0; i < count; i++) { ResolveInfo rInfo = matches.get(i); if (rInfo.activityInfo != null && defaultPackage.equals(rInfo.activityInfo.packageName)) { defaultRI = rInfo; break; } } } if (defaultRI != null) { // grant permission for default activity grantAccessoryPermission(accessory, defaultRI.activityInfo.applicationInfo.uid); // start default activity directly try { intent.setComponent( new ComponentName(defaultRI.activityInfo.packageName, defaultRI.activityInfo.name)); mContext.startActivity(intent); } catch (ActivityNotFoundException e) { Log.e(TAG, "startActivity failed", e); } } else { // start UsbResolverActivity so user can choose an activity Intent resolverIntent = new Intent(); resolverIntent.setClassName("com.android.systemui", "com.android.systemui.usb.UsbResolverActivity"); resolverIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); resolverIntent.putExtra(Intent.EXTRA_INTENT, intent); resolverIntent.putParcelableArrayListExtra("rlist", matches); try { mContext.startActivity(resolverIntent); } catch (ActivityNotFoundException e) { Log.e(TAG, "unable to start UsbResolverActivity"); } } } private boolean clearCompatibleMatchesLocked(String packageName, AccessoryFilter filter) { boolean changed = false; for (AccessoryFilter test : mAccessoryPreferenceMap.keySet()) { if (filter.matches(test)) { mAccessoryPreferenceMap.remove(test); changed = true; } } return changed; } private boolean handlePackageUpdateLocked(String packageName, ActivityInfo aInfo, String metaDataName) { XmlResourceParser parser = null; boolean changed = false; try { parser = aInfo.loadXmlMetaData(mPackageManager, metaDataName); if (parser == null) return false; XmlUtils.nextElement(parser); while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { String tagName = parser.getName(); if ("usb-accessory".equals(tagName)) { AccessoryFilter filter = AccessoryFilter.read(parser); if (clearCompatibleMatchesLocked(packageName, filter)) { changed = true; } } XmlUtils.nextElement(parser); } } catch (Exception e) { Log.w(TAG, "Unable to load component info " + aInfo.toString(), e); } finally { if (parser != null) parser.close(); } return changed; } // Check to see if the package supports any USB devices or accessories. // If so, clear any non-matching preferences for matching devices/accessories. private void handlePackageUpdate(String packageName) { synchronized (mLock) { PackageInfo info; boolean changed = false; try { info = mPackageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA); } catch (NameNotFoundException e) { Log.e(TAG, "handlePackageUpdate could not find package " + packageName, e); return; } ActivityInfo[] activities = info.activities; if (activities == null) return; for (int i = 0; i < activities.length; i++) { // check for meta-data, both for devices and accessories if (handlePackageUpdateLocked(packageName, activities[i], UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) { changed = true; } } if (changed) { writeSettingsLocked(); } } } public boolean hasPermission(UsbAccessory accessory) { synchronized (mLock) { SparseBooleanArray uidList = mAccessoryPermissionMap.get(accessory); if (uidList == null) { return false; } return uidList.get(Binder.getCallingUid()); } } public void checkPermission(UsbAccessory accessory) { if (!hasPermission(accessory)) { throw new SecurityException("User has not given permission to accessory " + accessory); } } private void requestPermissionDialog(Intent intent, String packageName, PendingIntent pi) { int uid = Binder.getCallingUid(); // compare uid with packageName to foil apps pretending to be someone else try { ApplicationInfo aInfo = mPackageManager.getApplicationInfo(packageName, 0); if (aInfo.uid != uid) { throw new IllegalArgumentException("package " + packageName + " does not match caller's uid " + uid); } } catch (PackageManager.NameNotFoundException e) { throw new IllegalArgumentException("package " + packageName + " not found"); } long identity = Binder.clearCallingIdentity(); intent.setClassName("com.android.systemui", "com.android.systemui.usb.UsbPermissionActivity"); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Intent.EXTRA_INTENT, pi); intent.putExtra("package", packageName); intent.putExtra("uid", uid); try { mContext.startActivity(intent); } catch (ActivityNotFoundException e) { Log.e(TAG, "unable to start UsbPermissionActivity"); } finally { Binder.restoreCallingIdentity(identity); } } public void requestPermission(UsbAccessory accessory, String packageName, PendingIntent pi) { Intent intent = new Intent(); // respond immediately if permission has already been granted if (hasPermission(accessory)) { intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory); intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true); try { pi.send(mContext, 0, intent); } catch (PendingIntent.CanceledException e) { Log.w(TAG, "requestPermission PendingIntent was cancelled"); } return; } intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory); requestPermissionDialog(intent, packageName, pi); } public void setAccessoryPackage(UsbAccessory accessory, String packageName) { AccessoryFilter filter = new AccessoryFilter(accessory); boolean changed = false; synchronized (mLock) { if (packageName == null) { changed = (mAccessoryPreferenceMap.remove(filter) != null); } else { changed = !packageName.equals(mAccessoryPreferenceMap.get(filter)); if (changed) { mAccessoryPreferenceMap.put(filter, packageName); } } if (changed) { writeSettingsLocked(); } } } public void grantAccessoryPermission(UsbAccessory accessory, int uid) { synchronized (mLock) { SparseBooleanArray uidList = mAccessoryPermissionMap.get(accessory); if (uidList == null) { uidList = new SparseBooleanArray(1); mAccessoryPermissionMap.put(accessory, uidList); } uidList.put(uid, true); } } public boolean hasDefaults(String packageName) { synchronized (mLock) { return mAccessoryPreferenceMap.values().contains(packageName); } } public void clearDefaults(String packageName) { synchronized (mLock) { if (clearPackageDefaultsLocked(packageName)) { writeSettingsLocked(); } } } private boolean clearPackageDefaultsLocked(String packageName) { boolean cleared = false; synchronized (mLock) { if (mAccessoryPreferenceMap.containsValue(packageName)) { // make a copy of the key set to avoid ConcurrentModificationException Object[] keys = mAccessoryPreferenceMap.keySet().toArray(); for (int i = 0; i < keys.length; i++) { Object key = keys[i]; if (packageName.equals(mAccessoryPreferenceMap.get(key))) { mAccessoryPreferenceMap.remove(key); cleared = true; } } } return cleared; } } public void dump(FileDescriptor fd, PrintWriter pw) { synchronized (mLock) { pw.println(" Accessory permissions:"); for (UsbAccessory accessory : mAccessoryPermissionMap.keySet()) { pw.print(" " + accessory + ": "); SparseBooleanArray uidList = mAccessoryPermissionMap.get(accessory); int count = uidList.size(); for (int i = 0; i < count; i++) { pw.print(Integer.toString(uidList.keyAt(i)) + " "); } pw.println(""); } pw.println(" Accessory preferences:"); for (AccessoryFilter filter : mAccessoryPreferenceMap.keySet()) { pw.println(" " + filter + ": " + mAccessoryPreferenceMap.get(filter)); } } } }