UsbDeviceSettingsManager.java revision 2cc0377200b94b2f68f34e34554f2aa39e09cbce
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.usb;
18
19import android.app.PendingIntent;
20import android.content.ActivityNotFoundException;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.ActivityInfo;
25import android.content.pm.ApplicationInfo;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageManager;
28import android.content.pm.PackageManager.NameNotFoundException;
29import android.content.pm.ResolveInfo;
30import android.content.res.XmlResourceParser;
31import android.hardware.usb.UsbAccessory;
32import android.hardware.usb.UsbManager;
33import android.os.Binder;
34import android.os.FileUtils;
35import android.os.Process;
36import android.util.Log;
37import android.util.SparseBooleanArray;
38import android.util.Xml;
39
40import com.android.internal.content.PackageMonitor;
41import com.android.internal.util.FastXmlSerializer;
42import com.android.internal.util.XmlUtils;
43
44import org.xmlpull.v1.XmlPullParser;
45import org.xmlpull.v1.XmlPullParserException;
46import org.xmlpull.v1.XmlSerializer;
47
48import java.io.BufferedOutputStream;
49import java.io.File;
50import java.io.FileDescriptor;
51import java.io.FileInputStream;
52import java.io.FileNotFoundException;
53import java.io.FileOutputStream;
54import java.io.IOException;
55import java.io.PrintWriter;
56import java.util.ArrayList;
57import java.util.HashMap;
58import java.util.List;
59
60class UsbDeviceSettingsManager {
61
62    private static final String TAG = "UsbDeviceSettingsManager";
63    private static final File sSettingsFile = new File("/data/system/usb_device_manager.xml");
64
65    private final Context mContext;
66    private final PackageManager mPackageManager;
67
68    // Temporary mapping UsbAccessory to list of UIDs with permissions for the accessory
69    private final HashMap<UsbAccessory, SparseBooleanArray> mAccessoryPermissionMap =
70            new HashMap<UsbAccessory, SparseBooleanArray>();
71    // Maps AccessoryFilter to user preferred application package
72    private final HashMap<AccessoryFilter, String> mAccessoryPreferenceMap =
73            new HashMap<AccessoryFilter, String>();
74
75    private final Object mLock = new Object();
76
77    // This class is used to describe a USB accessory.
78    // When used in HashMaps all values must be specified,
79    // but wildcards can be used for any of the fields in
80    // the package meta-data.
81    private static class AccessoryFilter {
82        // USB accessory manufacturer (or null for unspecified)
83        public final String mManufacturer;
84        // USB accessory model (or null for unspecified)
85        public final String mModel;
86        // USB accessory version (or null for unspecified)
87        public final String mVersion;
88
89        public AccessoryFilter(String manufacturer, String model, String version) {
90            mManufacturer = manufacturer;
91            mModel = model;
92            mVersion = version;
93        }
94
95        public AccessoryFilter(UsbAccessory accessory) {
96            mManufacturer = accessory.getManufacturer();
97            mModel = accessory.getModel();
98            mVersion = accessory.getVersion();
99        }
100
101        public static AccessoryFilter read(XmlPullParser parser)
102                throws XmlPullParserException, IOException {
103            String manufacturer = null;
104            String model = null;
105            String version = null;
106
107            int count = parser.getAttributeCount();
108            for (int i = 0; i < count; i++) {
109                String name = parser.getAttributeName(i);
110                String value = parser.getAttributeValue(i);
111
112                if ("manufacturer".equals(name)) {
113                    manufacturer = value;
114                } else if ("model".equals(name)) {
115                    model = value;
116                } else if ("version".equals(name)) {
117                    version = value;
118                }
119             }
120             return new AccessoryFilter(manufacturer, model, version);
121        }
122
123        public void write(XmlSerializer serializer)throws IOException {
124            serializer.startTag(null, "usb-accessory");
125            if (mManufacturer != null) {
126                serializer.attribute(null, "manufacturer", mManufacturer);
127            }
128            if (mModel != null) {
129                serializer.attribute(null, "model", mModel);
130            }
131            if (mVersion != null) {
132                serializer.attribute(null, "version", mVersion);
133            }
134            serializer.endTag(null, "usb-accessory");
135        }
136
137        public boolean matches(UsbAccessory acc) {
138            if (mManufacturer != null && !acc.getManufacturer().equals(mManufacturer)) return false;
139            if (mModel != null && !acc.getModel().equals(mModel)) return false;
140            if (mVersion != null && !acc.getVersion().equals(mVersion)) return false;
141            return true;
142        }
143
144        public boolean matches(AccessoryFilter f) {
145            if (mManufacturer != null && !f.mManufacturer.equals(mManufacturer)) return false;
146            if (mModel != null && !f.mModel.equals(mModel)) return false;
147            if (mVersion != null && !f.mVersion.equals(mVersion)) return false;
148            return true;
149        }
150
151        @Override
152        public boolean equals(Object obj) {
153            // can't compare if we have wildcard strings
154            if (mManufacturer == null || mModel == null || mVersion == null) {
155                return false;
156            }
157            if (obj instanceof AccessoryFilter) {
158                AccessoryFilter filter = (AccessoryFilter)obj;
159                return (mManufacturer.equals(filter.mManufacturer) &&
160                        mModel.equals(filter.mModel) &&
161                        mVersion.equals(filter.mVersion));
162            }
163            if (obj instanceof UsbAccessory) {
164                UsbAccessory accessory = (UsbAccessory)obj;
165                return (mManufacturer.equals(accessory.getManufacturer()) &&
166                        mModel.equals(accessory.getModel()) &&
167                        mVersion.equals(accessory.getVersion()));
168            }
169            return false;
170        }
171
172        @Override
173        public int hashCode() {
174            return ((mManufacturer == null ? 0 : mManufacturer.hashCode()) ^
175                    (mModel == null ? 0 : mModel.hashCode()) ^
176                    (mVersion == null ? 0 : mVersion.hashCode()));
177        }
178
179        @Override
180        public String toString() {
181            return "AccessoryFilter[mManufacturer=\"" + mManufacturer +
182                                "\", mModel=\"" + mModel +
183                                "\", mVersion=\"" + mVersion + "\"]";
184        }
185    }
186
187    private class MyPackageMonitor extends PackageMonitor {
188
189        public void onPackageAdded(String packageName, int uid) {
190            handlePackageUpdate(packageName);
191        }
192
193        public void onPackageChanged(String packageName, int uid, String[] components) {
194            handlePackageUpdate(packageName);
195        }
196
197        public void onPackageRemoved(String packageName, int uid) {
198            clearDefaults(packageName);
199        }
200    }
201    MyPackageMonitor mPackageMonitor = new MyPackageMonitor();
202
203    public UsbDeviceSettingsManager(Context context) {
204        mContext = context;
205        mPackageManager = context.getPackageManager();
206        synchronized (mLock) {
207            readSettingsLocked();
208        }
209        mPackageMonitor.register(context, true);
210    }
211
212    private void readPreference(XmlPullParser parser)
213            throws XmlPullParserException, IOException {
214        String packageName = null;
215        int count = parser.getAttributeCount();
216        for (int i = 0; i < count; i++) {
217            if ("package".equals(parser.getAttributeName(i))) {
218                packageName = parser.getAttributeValue(i);
219                break;
220            }
221        }
222        XmlUtils.nextElement(parser);
223        if ("usb-accessory".equals(parser.getName())) {
224            AccessoryFilter filter = AccessoryFilter.read(parser);
225            mAccessoryPreferenceMap.put(filter, packageName);
226        }
227        XmlUtils.nextElement(parser);
228    }
229
230    private void readSettingsLocked() {
231        FileInputStream stream = null;
232        try {
233            stream = new FileInputStream(sSettingsFile);
234            XmlPullParser parser = Xml.newPullParser();
235            parser.setInput(stream, null);
236
237            XmlUtils.nextElement(parser);
238            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
239                String tagName = parser.getName();
240                if ("preference".equals(tagName)) {
241                    readPreference(parser);
242                 } else {
243                    XmlUtils.nextElement(parser);
244                }
245            }
246        } catch (FileNotFoundException e) {
247            Log.w(TAG, "settings file not found");
248        } catch (Exception e) {
249            Log.e(TAG, "error reading settings file, deleting to start fresh", e);
250            sSettingsFile.delete();
251        } finally {
252            if (stream != null) {
253                try {
254                    stream.close();
255                } catch (IOException e) {
256                }
257            }
258        }
259    }
260
261    private void writeSettingsLocked() {
262        FileOutputStream fos = null;
263        try {
264            FileOutputStream fstr = new FileOutputStream(sSettingsFile);
265            Log.d(TAG, "writing settings to " + fstr);
266            BufferedOutputStream str = new BufferedOutputStream(fstr);
267            FastXmlSerializer serializer = new FastXmlSerializer();
268            serializer.setOutput(str, "utf-8");
269            serializer.startDocument(null, true);
270            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
271            serializer.startTag(null, "settings");
272
273            for (AccessoryFilter filter : mAccessoryPreferenceMap.keySet()) {
274                serializer.startTag(null, "preference");
275                serializer.attribute(null, "package", mAccessoryPreferenceMap.get(filter));
276                filter.write(serializer);
277                serializer.endTag(null, "preference");
278            }
279
280            serializer.endTag(null, "settings");
281            serializer.endDocument();
282
283            str.flush();
284            FileUtils.sync(fstr);
285            str.close();
286        } catch (Exception e) {
287            Log.e(TAG, "error writing settings file, deleting to start fresh", e);
288            sSettingsFile.delete();
289        }
290    }
291
292    // Checks to see if a package matches an accessory.
293    private boolean packageMatchesLocked(ResolveInfo info, String metaDataName,
294            UsbAccessory accessory) {
295        ActivityInfo ai = info.activityInfo;
296
297        XmlResourceParser parser = null;
298        try {
299            parser = ai.loadXmlMetaData(mPackageManager, metaDataName);
300            if (parser == null) {
301                Log.w(TAG, "no meta-data for " + info);
302                return false;
303            }
304
305            XmlUtils.nextElement(parser);
306            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
307                String tagName = parser.getName();
308                if (accessory != null && "usb-accessory".equals(tagName)) {
309                    AccessoryFilter filter = AccessoryFilter.read(parser);
310                    if (filter.matches(accessory)) {
311                        return true;
312                    }
313                }
314                XmlUtils.nextElement(parser);
315            }
316        } catch (Exception e) {
317            Log.w(TAG, "Unable to load component info " + info.toString(), e);
318        } finally {
319            if (parser != null) parser.close();
320        }
321        return false;
322    }
323
324    private final ArrayList<ResolveInfo> getAccessoryMatchesLocked(
325            UsbAccessory accessory, Intent intent) {
326        ArrayList<ResolveInfo> matches = new ArrayList<ResolveInfo>();
327        List<ResolveInfo> resolveInfos = mPackageManager.queryIntentActivities(intent,
328                PackageManager.GET_META_DATA);
329        int count = resolveInfos.size();
330        for (int i = 0; i < count; i++) {
331            ResolveInfo resolveInfo = resolveInfos.get(i);
332            if (packageMatchesLocked(resolveInfo, intent.getAction(), accessory)) {
333                matches.add(resolveInfo);
334            }
335        }
336        return matches;
337    }
338
339    public void accessoryAttached(UsbAccessory accessory) {
340        Intent intent = new Intent(UsbManager.ACTION_USB_ACCESSORY_ATTACHED);
341        intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory);
342        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
343
344        ArrayList<ResolveInfo> matches;
345        String defaultPackage;
346        synchronized (mLock) {
347            matches = getAccessoryMatchesLocked(accessory, intent);
348            // Launch our default activity directly, if we have one.
349            // Otherwise we will start the UsbResolverActivity to allow the user to choose.
350            defaultPackage = mAccessoryPreferenceMap.get(new AccessoryFilter(accessory));
351        }
352
353        resolveActivity(intent, matches, defaultPackage, accessory);
354    }
355
356    public void accessoryDetached(UsbAccessory accessory) {
357        // clear temporary permissions for the accessory
358        mAccessoryPermissionMap.remove(accessory);
359
360        Intent intent = new Intent(
361                UsbManager.ACTION_USB_ACCESSORY_DETACHED);
362        intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory);
363        mContext.sendBroadcast(intent);
364    }
365
366    private void resolveActivity(Intent intent, ArrayList<ResolveInfo> matches,
367            String defaultPackage, UsbAccessory accessory) {
368        int count = matches.size();
369
370        // don't show the resolver activity if there are no choices available
371        if (count == 0) {
372            if (accessory != null) {
373                String uri = accessory.getUri();
374                if (uri != null && uri.length() > 0) {
375                    // display URI to user
376                    // start UsbResolverActivity so user can choose an activity
377                    Intent dialogIntent = new Intent();
378                    dialogIntent.setClassName("com.android.systemui",
379                            "com.android.systemui.usb.UsbAccessoryUriActivity");
380                    dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
381                    dialogIntent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory);
382                    dialogIntent.putExtra("uri", uri);
383                    try {
384                        mContext.startActivity(dialogIntent);
385                    } catch (ActivityNotFoundException e) {
386                        Log.e(TAG, "unable to start UsbAccessoryUriActivity");
387                    }
388                }
389            }
390
391            // do nothing
392            return;
393        }
394
395        ResolveInfo defaultRI = null;
396        if (count == 1 && defaultPackage == null) {
397            // Check to see if our single choice is on the system partition.
398            // If so, treat it as our default without calling UsbResolverActivity
399            ResolveInfo rInfo = matches.get(0);
400            if (rInfo.activityInfo != null &&
401                    rInfo.activityInfo.applicationInfo != null &&
402                    (rInfo.activityInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
403                defaultRI = rInfo;
404            }
405        }
406
407        if (defaultRI == null && defaultPackage != null) {
408            // look for default activity
409            for (int i = 0; i < count; i++) {
410                ResolveInfo rInfo = matches.get(i);
411                if (rInfo.activityInfo != null &&
412                        defaultPackage.equals(rInfo.activityInfo.packageName)) {
413                    defaultRI = rInfo;
414                    break;
415                }
416            }
417        }
418
419        if (defaultRI != null) {
420            // grant permission for default activity
421            grantAccessoryPermission(accessory, defaultRI.activityInfo.applicationInfo.uid);
422
423            // start default activity directly
424            try {
425                intent.setComponent(
426                        new ComponentName(defaultRI.activityInfo.packageName,
427                                defaultRI.activityInfo.name));
428                mContext.startActivity(intent);
429            } catch (ActivityNotFoundException e) {
430                Log.e(TAG, "startActivity failed", e);
431            }
432        } else {
433            // start UsbResolverActivity so user can choose an activity
434            Intent resolverIntent = new Intent();
435            resolverIntent.setClassName("com.android.systemui",
436                    "com.android.systemui.usb.UsbResolverActivity");
437            resolverIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
438            resolverIntent.putExtra(Intent.EXTRA_INTENT, intent);
439            resolverIntent.putParcelableArrayListExtra("rlist", matches);
440            try {
441                mContext.startActivity(resolverIntent);
442            } catch (ActivityNotFoundException e) {
443                Log.e(TAG, "unable to start UsbResolverActivity");
444            }
445        }
446    }
447
448    private boolean clearCompatibleMatchesLocked(String packageName, AccessoryFilter filter) {
449        boolean changed = false;
450        for (AccessoryFilter test : mAccessoryPreferenceMap.keySet()) {
451            if (filter.matches(test)) {
452                mAccessoryPreferenceMap.remove(test);
453                changed = true;
454            }
455        }
456        return changed;
457    }
458
459    private boolean handlePackageUpdateLocked(String packageName, ActivityInfo aInfo,
460            String metaDataName) {
461        XmlResourceParser parser = null;
462        boolean changed = false;
463
464        try {
465            parser = aInfo.loadXmlMetaData(mPackageManager, metaDataName);
466            if (parser == null) return false;
467
468            XmlUtils.nextElement(parser);
469            while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
470                String tagName = parser.getName();
471                if ("usb-accessory".equals(tagName)) {
472                    AccessoryFilter filter = AccessoryFilter.read(parser);
473                    if (clearCompatibleMatchesLocked(packageName, filter)) {
474                        changed = true;
475                    }
476                }
477                XmlUtils.nextElement(parser);
478            }
479        } catch (Exception e) {
480            Log.w(TAG, "Unable to load component info " + aInfo.toString(), e);
481        } finally {
482            if (parser != null) parser.close();
483        }
484        return changed;
485    }
486
487    // Check to see if the package supports any USB devices or accessories.
488    // If so, clear any non-matching preferences for matching devices/accessories.
489    private void handlePackageUpdate(String packageName) {
490        synchronized (mLock) {
491            PackageInfo info;
492            boolean changed = false;
493
494            try {
495                info = mPackageManager.getPackageInfo(packageName,
496                        PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
497            } catch (NameNotFoundException e) {
498                Log.e(TAG, "handlePackageUpdate could not find package " + packageName, e);
499                return;
500            }
501
502            ActivityInfo[] activities = info.activities;
503            if (activities == null) return;
504            for (int i = 0; i < activities.length; i++) {
505                // check for meta-data, both for devices and accessories
506                if (handlePackageUpdateLocked(packageName, activities[i],
507                        UsbManager.ACTION_USB_ACCESSORY_ATTACHED)) {
508                    changed = true;
509                }
510            }
511
512            if (changed) {
513                writeSettingsLocked();
514            }
515        }
516    }
517
518    public boolean hasPermission(UsbAccessory accessory) {
519        synchronized (mLock) {
520            SparseBooleanArray uidList = mAccessoryPermissionMap.get(accessory);
521            if (uidList == null) {
522                return false;
523            }
524            return uidList.get(Binder.getCallingUid());
525        }
526    }
527
528    public void checkPermission(UsbAccessory accessory) {
529        if (!hasPermission(accessory)) {
530            throw new SecurityException("User has not given permission to accessory " + accessory);
531        }
532    }
533
534    private void requestPermissionDialog(Intent intent, String packageName, PendingIntent pi) {
535        int uid = Binder.getCallingUid();
536
537        // compare uid with packageName to foil apps pretending to be someone else
538        try {
539            ApplicationInfo aInfo = mPackageManager.getApplicationInfo(packageName, 0);
540            if (aInfo.uid != uid) {
541                throw new IllegalArgumentException("package " + packageName +
542                        " does not match caller's uid " + uid);
543            }
544        } catch (PackageManager.NameNotFoundException e) {
545            throw new IllegalArgumentException("package " + packageName + " not found");
546        }
547
548        long identity = Binder.clearCallingIdentity();
549        intent.setClassName("com.android.systemui",
550                "com.android.systemui.usb.UsbPermissionActivity");
551        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
552        intent.putExtra(Intent.EXTRA_INTENT, pi);
553        intent.putExtra("package", packageName);
554        intent.putExtra("uid", uid);
555        try {
556            mContext.startActivity(intent);
557        } catch (ActivityNotFoundException e) {
558            Log.e(TAG, "unable to start UsbPermissionActivity");
559        } finally {
560            Binder.restoreCallingIdentity(identity);
561        }
562    }
563
564    public void requestPermission(UsbAccessory accessory, String packageName, PendingIntent pi) {
565      Intent intent = new Intent();
566
567        // respond immediately if permission has already been granted
568        if (hasPermission(accessory)) {
569            intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory);
570            intent.putExtra(UsbManager.EXTRA_PERMISSION_GRANTED, true);
571           try {
572                pi.send(mContext, 0, intent);
573            } catch (PendingIntent.CanceledException e) {
574                Log.w(TAG, "requestPermission PendingIntent was cancelled");
575            }
576            return;
577        }
578
579        intent.putExtra(UsbManager.EXTRA_ACCESSORY, accessory);
580        requestPermissionDialog(intent, packageName, pi);
581    }
582
583    public void setAccessoryPackage(UsbAccessory accessory, String packageName) {
584        AccessoryFilter filter = new AccessoryFilter(accessory);
585        boolean changed = false;
586        synchronized (mLock) {
587            if (packageName == null) {
588                changed = (mAccessoryPreferenceMap.remove(filter) != null);
589            } else {
590                changed = !packageName.equals(mAccessoryPreferenceMap.get(filter));
591                if (changed) {
592                    mAccessoryPreferenceMap.put(filter, packageName);
593                }
594            }
595            if (changed) {
596                writeSettingsLocked();
597            }
598        }
599    }
600
601    public void grantAccessoryPermission(UsbAccessory accessory, int uid) {
602        synchronized (mLock) {
603            SparseBooleanArray uidList = mAccessoryPermissionMap.get(accessory);
604            if (uidList == null) {
605                uidList = new SparseBooleanArray(1);
606                mAccessoryPermissionMap.put(accessory, uidList);
607            }
608            uidList.put(uid, true);
609        }
610    }
611
612    public boolean hasDefaults(String packageName) {
613        synchronized (mLock) {
614            return mAccessoryPreferenceMap.values().contains(packageName);
615        }
616    }
617
618    public void clearDefaults(String packageName) {
619        synchronized (mLock) {
620            if (clearPackageDefaultsLocked(packageName)) {
621                writeSettingsLocked();
622            }
623        }
624    }
625
626    private boolean clearPackageDefaultsLocked(String packageName) {
627        boolean cleared = false;
628        synchronized (mLock) {
629            if (mAccessoryPreferenceMap.containsValue(packageName)) {
630                // make a copy of the key set to avoid ConcurrentModificationException
631                Object[] keys = mAccessoryPreferenceMap.keySet().toArray();
632                for (int i = 0; i < keys.length; i++) {
633                    Object key = keys[i];
634                    if (packageName.equals(mAccessoryPreferenceMap.get(key))) {
635                        mAccessoryPreferenceMap.remove(key);
636                        cleared = true;
637                    }
638                }
639            }
640            return cleared;
641        }
642    }
643
644    public void dump(FileDescriptor fd, PrintWriter pw) {
645        synchronized (mLock) {
646            pw.println("  Accessory permissions:");
647            for (UsbAccessory accessory : mAccessoryPermissionMap.keySet()) {
648                pw.print("    " + accessory + ": ");
649                SparseBooleanArray uidList = mAccessoryPermissionMap.get(accessory);
650                int count = uidList.size();
651                for (int i = 0; i < count; i++) {
652                    pw.print(Integer.toString(uidList.keyAt(i)) + " ");
653                }
654                pw.println("");
655            }
656            pw.println("  Accessory preferences:");
657            for (AccessoryFilter filter : mAccessoryPreferenceMap.keySet()) {
658                pw.println("    " + filter + ": " + mAccessoryPreferenceMap.get(filter));
659            }
660        }
661    }
662}
663