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 an
14 * limitations under the License.
15 */
16
17package com.android.server.usb;
18
19import android.alsa.AlsaCardsParser;
20import android.alsa.AlsaDevicesParser;
21import android.content.Context;
22import android.content.Intent;
23import android.hardware.usb.UsbConfiguration;
24import android.hardware.usb.UsbConstants;
25import android.hardware.usb.UsbDevice;
26import android.hardware.usb.UsbEndpoint;
27import android.hardware.usb.UsbInterface;
28import android.media.AudioManager;
29import android.os.Bundle;
30import android.os.ParcelFileDescriptor;
31import android.os.Parcelable;
32import android.os.UserHandle;
33import android.util.Slog;
34
35import com.android.internal.annotations.GuardedBy;
36
37import java.io.File;
38import java.io.FileDescriptor;
39import java.io.FileNotFoundException;
40import java.io.PrintWriter;
41import java.util.ArrayList;
42import java.util.HashMap;
43
44/**
45 * UsbHostManager manages USB state in host mode.
46 */
47public class UsbHostManager {
48    private static final String TAG = UsbHostManager.class.getSimpleName();
49    private static final boolean DEBUG_AUDIO = false;
50
51    // contains all connected USB devices
52    private final HashMap<String, UsbDevice> mDevices = new HashMap<String, UsbDevice>();
53
54    // USB busses to exclude from USB host support
55    private final String[] mHostBlacklist;
56
57    private final Context mContext;
58    private final Object mLock = new Object();
59
60    private UsbDevice mNewDevice;
61    private UsbConfiguration mNewConfiguration;
62    private UsbInterface mNewInterface;
63    private ArrayList<UsbConfiguration> mNewConfigurations;
64    private ArrayList<UsbInterface> mNewInterfaces;
65    private ArrayList<UsbEndpoint> mNewEndpoints;
66
67    // Attributes of any connected USB audio device.
68    //TODO(pmclean) When we extend to multiple, USB Audio devices, we will need to get
69    // more clever about this.
70    private int mConnectedUsbCard = -1;
71    private int mConnectedUsbDeviceNum = -1;
72    private boolean mConnectedHasPlayback = false;
73    private boolean mConnectedHasCapture = false;
74    private boolean mConnectedHasMIDI = false;
75
76    @GuardedBy("mLock")
77    private UsbSettingsManager mCurrentSettings;
78
79    public UsbHostManager(Context context) {
80        mContext = context;
81        mHostBlacklist = context.getResources().getStringArray(
82                com.android.internal.R.array.config_usbHostBlacklist);
83    }
84
85    public void setCurrentSettings(UsbSettingsManager settings) {
86        synchronized (mLock) {
87            mCurrentSettings = settings;
88        }
89    }
90
91    private UsbSettingsManager getCurrentSettings() {
92        synchronized (mLock) {
93            return mCurrentSettings;
94        }
95    }
96
97    private boolean isBlackListed(String deviceName) {
98        int count = mHostBlacklist.length;
99        for (int i = 0; i < count; i++) {
100            if (deviceName.startsWith(mHostBlacklist[i])) {
101                return true;
102            }
103        }
104        return false;
105    }
106
107    /* returns true if the USB device should not be accessible by applications */
108    private boolean isBlackListed(int clazz, int subClass, int protocol) {
109        // blacklist hubs
110        if (clazz == UsbConstants.USB_CLASS_HUB) return true;
111
112        // blacklist HID boot devices (mouse and keyboard)
113        if (clazz == UsbConstants.USB_CLASS_HID &&
114                subClass == UsbConstants.USB_INTERFACE_SUBCLASS_BOOT) {
115            return true;
116        }
117
118        return false;
119    }
120
121    // Broadcasts the arrival/departure of a USB audio interface
122    // card - the ALSA card number of the physical interface
123    // device - the ALSA device number of the physical interface
124    // enabled - if true, we're connecting a device (it's arrived), else disconnecting
125    private void sendDeviceNotification(int card, int device, boolean enabled,
126            boolean hasPlayback, boolean hasCapture, boolean hasMIDI) {
127        // send a sticky broadcast containing current USB state
128        Intent intent = new Intent(AudioManager.ACTION_USB_AUDIO_DEVICE_PLUG);
129        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
130        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
131        intent.putExtra("state", enabled ? 1 : 0);
132        intent.putExtra("card", card);
133        intent.putExtra("device", device);
134        intent.putExtra("hasPlayback", hasPlayback);
135        intent.putExtra("hasCapture", hasCapture);
136        intent.putExtra("hasMIDI", hasMIDI);
137        mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
138    }
139
140    private boolean waitForAlsaFile(int card, int device, boolean capture) {
141        // These values were empirically determined.
142        final int kNumRetries = 5;
143        final int kSleepTime = 500; // ms
144        String alsaDevPath = "/dev/snd/pcmC" + card + "D" + device + (capture ? "c" : "p");
145        File alsaDevFile = new File(alsaDevPath);
146        boolean exists = false;
147        for (int retry = 0; !exists && retry < kNumRetries; retry++) {
148            exists = alsaDevFile.exists();
149            if (!exists) {
150                try {
151                    Thread.sleep(kSleepTime);
152                } catch (IllegalThreadStateException ex) {
153                    Slog.d(TAG, "usb: IllegalThreadStateException while waiting for ALSA file.");
154                } catch (java.lang.InterruptedException ex) {
155                    Slog.d(TAG, "usb: InterruptedException while waiting for ALSA file.");
156                }
157            }
158        }
159
160        return exists;
161    }
162
163    /* Called from JNI in monitorUsbHostBus() to report new USB devices
164       Returns true if successful, in which case the JNI code will continue adding configurations,
165       interfaces and endpoints, and finally call endUsbDeviceAdded after all descriptors
166       have been processed
167     */
168    private boolean beginUsbDeviceAdded(String deviceName, int vendorID, int productID,
169            int deviceClass, int deviceSubclass, int deviceProtocol,
170            String manufacturerName, String productName, String serialNumber) {
171
172        if (DEBUG_AUDIO) {
173            Slog.d(TAG, "usb:UsbHostManager.beginUsbDeviceAdded(" + deviceName + ")");
174            // Audio Class Codes:
175            // Audio: 0x01
176            // Audio Subclass Codes:
177            // undefined: 0x00
178            // audio control: 0x01
179            // audio streaming: 0x02
180            // midi streaming: 0x03
181
182            // some useful debugging info
183            Slog.d(TAG, "usb: nm:" + deviceName + " vnd:" + vendorID + " prd:" + productID + " cls:"
184                    + deviceClass + " sub:" + deviceSubclass + " proto:" + deviceProtocol);
185        }
186
187        // OK this is non-obvious, but true. One can't tell if the device being attached is even
188        // potentially an audio device without parsing the interface descriptors, so punt on any
189        // such test until endUsbDeviceAdded() when we have that info.
190
191        if (isBlackListed(deviceName) ||
192                isBlackListed(deviceClass, deviceSubclass, deviceProtocol)) {
193            return false;
194        }
195
196        synchronized (mLock) {
197            if (mDevices.get(deviceName) != null) {
198                Slog.w(TAG, "device already on mDevices list: " + deviceName);
199                return false;
200            }
201
202            if (mNewDevice != null) {
203                Slog.e(TAG, "mNewDevice is not null in endUsbDeviceAdded");
204                return false;
205            }
206
207            mNewDevice = new UsbDevice(deviceName, vendorID, productID,
208                    deviceClass, deviceSubclass, deviceProtocol,
209                    manufacturerName, productName, serialNumber);
210
211            mNewConfigurations = new ArrayList<UsbConfiguration>();
212            mNewInterfaces = new ArrayList<UsbInterface>();
213            mNewEndpoints = new ArrayList<UsbEndpoint>();
214        }
215
216        return true;
217    }
218
219    /* Called from JNI in monitorUsbHostBus() to report new USB configuration for the device
220       currently being added.  Returns true if successful, false in case of error.
221     */
222    private void addUsbConfiguration(int id, String name, int attributes, int maxPower) {
223        if (mNewConfiguration != null) {
224            mNewConfiguration.setInterfaces(
225                    mNewInterfaces.toArray(new UsbInterface[mNewInterfaces.size()]));
226            mNewInterfaces.clear();
227        }
228
229        mNewConfiguration = new UsbConfiguration(id, name, attributes, maxPower);
230        mNewConfigurations.add(mNewConfiguration);
231    }
232
233    /* Called from JNI in monitorUsbHostBus() to report new USB interface for the device
234       currently being added.  Returns true if successful, false in case of error.
235     */
236    private void addUsbInterface(int id, String name, int altSetting,
237            int Class, int subClass, int protocol) {
238        if (mNewInterface != null) {
239            mNewInterface.setEndpoints(
240                    mNewEndpoints.toArray(new UsbEndpoint[mNewEndpoints.size()]));
241            mNewEndpoints.clear();
242        }
243
244        mNewInterface = new UsbInterface(id, altSetting, name, Class, subClass, protocol);
245        mNewInterfaces.add(mNewInterface);
246    }
247
248    /* Called from JNI in monitorUsbHostBus() to report new USB endpoint for the device
249       currently being added.  Returns true if successful, false in case of error.
250     */
251    private void addUsbEndpoint(int address, int attributes, int maxPacketSize, int interval) {
252        mNewEndpoints.add(new UsbEndpoint(address, attributes, maxPacketSize, interval));
253    }
254
255    /* Called from JNI in monitorUsbHostBus() to finish adding a new device */
256    private void endUsbDeviceAdded() {
257        if (DEBUG_AUDIO) {
258            Slog.d(TAG, "usb:UsbHostManager.endUsbDeviceAdded()");
259        }
260        if (mNewInterface != null) {
261            mNewInterface.setEndpoints(
262                    mNewEndpoints.toArray(new UsbEndpoint[mNewEndpoints.size()]));
263        }
264        if (mNewConfiguration != null) {
265            mNewConfiguration.setInterfaces(
266                    mNewInterfaces.toArray(new UsbInterface[mNewInterfaces.size()]));
267        }
268
269        // Is there an audio interface in there?
270        final int kUsbClassId_Audio = 0x01;
271        boolean isAudioDevice = false;
272        for (int ntrfaceIndex = 0; !isAudioDevice && ntrfaceIndex < mNewInterfaces.size();
273                ntrfaceIndex++) {
274            UsbInterface ntrface = mNewInterfaces.get(ntrfaceIndex);
275            if (ntrface.getInterfaceClass() == kUsbClassId_Audio) {
276                isAudioDevice = true;
277            }
278        }
279
280        synchronized (mLock) {
281            if (mNewDevice != null) {
282                mNewDevice.setConfigurations(
283                        mNewConfigurations.toArray(new UsbConfiguration[mNewConfigurations.size()]));
284                mDevices.put(mNewDevice.getDeviceName(), mNewDevice);
285                Slog.d(TAG, "Added device " + mNewDevice);
286                getCurrentSettings().deviceAttached(mNewDevice);
287            } else {
288                Slog.e(TAG, "mNewDevice is null in endUsbDeviceAdded");
289            }
290            mNewDevice = null;
291            mNewConfigurations = null;
292            mNewInterfaces = null;
293            mNewEndpoints = null;
294        }
295
296        if (!isAudioDevice) {
297            return; // bail
298        }
299
300        //TODO(pmclean) The "Parser" objects inspect files in "/proc/asound" which we presume is
301        // present, unlike the waitForAlsaFile() which waits on a file in /dev/snd. It is not
302        // clear why this works, or that it can be relied on going forward.  Needs further
303        // research.
304        AlsaCardsParser cardsParser = new AlsaCardsParser();
305        cardsParser.scan();
306        // cardsParser.Log();
307
308        // But we need to parse the device to determine its capabilities.
309        AlsaDevicesParser devicesParser = new AlsaDevicesParser();
310        devicesParser.scan();
311        // devicesParser.Log();
312
313        // The protocol for now will be to select the last-connected (highest-numbered)
314        // Alsa Card.
315        mConnectedUsbCard = cardsParser.getNumCardRecords() - 1;
316        mConnectedUsbDeviceNum = 0;
317
318        mConnectedHasPlayback = devicesParser.hasPlaybackDevices(mConnectedUsbCard);
319        mConnectedHasCapture = devicesParser.hasCaptureDevices(mConnectedUsbCard);
320        mConnectedHasMIDI = devicesParser.hasMIDIDevices(mConnectedUsbCard);
321
322        // Playback device file needed/present?
323        if (mConnectedHasPlayback &&
324            !waitForAlsaFile(mConnectedUsbCard, mConnectedUsbDeviceNum, false)) {
325            return;
326        }
327
328        // Capture device file needed/present?
329        if (mConnectedHasCapture &&
330            !waitForAlsaFile(mConnectedUsbCard, mConnectedUsbDeviceNum, true)) {
331            return;
332        }
333
334        if (DEBUG_AUDIO) {
335            Slog.d(TAG,
336                    "usb: hasPlayback:" + mConnectedHasPlayback + " hasCapture:" + mConnectedHasCapture);
337        }
338
339        sendDeviceNotification(mConnectedUsbCard,
340                mConnectedUsbDeviceNum,
341                true,
342                mConnectedHasPlayback,
343                mConnectedHasCapture,
344                mConnectedHasMIDI);
345    }
346
347    /* Called from JNI in monitorUsbHostBus to report USB device removal */
348    private void usbDeviceRemoved(String deviceName) {
349        if (DEBUG_AUDIO) {
350          Slog.d(TAG, "usb:UsbHostManager.usbDeviceRemoved() nm:" + deviceName);
351        }
352
353        if (mConnectedUsbCard != -1 && mConnectedUsbDeviceNum != -1) {
354            sendDeviceNotification(mConnectedUsbCard,
355                    mConnectedUsbDeviceNum,
356                    false,
357                    mConnectedHasPlayback,
358                    mConnectedHasCapture,
359                    mConnectedHasMIDI);
360            mConnectedUsbCard = -1;
361            mConnectedUsbDeviceNum = -1;
362            mConnectedHasPlayback = false;
363            mConnectedHasCapture = false;
364            mConnectedHasMIDI = false;
365        }
366
367        synchronized (mLock) {
368            UsbDevice device = mDevices.remove(deviceName);
369            if (device != null) {
370                getCurrentSettings().deviceDetached(device);
371            }
372        }
373    }
374
375    public void systemReady() {
376        synchronized (mLock) {
377            // Create a thread to call into native code to wait for USB host events.
378            // This thread will call us back on usbDeviceAdded and usbDeviceRemoved.
379            Runnable runnable = new Runnable() {
380                public void run() {
381                    monitorUsbHostBus();
382                }
383            };
384            new Thread(null, runnable, "UsbService host thread").start();
385        }
386    }
387
388    /* Returns a list of all currently attached USB devices */
389    public void getDeviceList(Bundle devices) {
390        synchronized (mLock) {
391            for (String name : mDevices.keySet()) {
392                devices.putParcelable(name, mDevices.get(name));
393            }
394        }
395    }
396
397    /* Opens the specified USB device */
398    public ParcelFileDescriptor openDevice(String deviceName) {
399        synchronized (mLock) {
400            if (isBlackListed(deviceName)) {
401                throw new SecurityException("USB device is on a restricted bus");
402            }
403            UsbDevice device = mDevices.get(deviceName);
404            if (device == null) {
405                // if it is not in mDevices, it either does not exist or is blacklisted
406                throw new IllegalArgumentException(
407                        "device " + deviceName + " does not exist or is restricted");
408            }
409            getCurrentSettings().checkPermission(device);
410            return nativeOpenDevice(deviceName);
411        }
412    }
413
414    public void dump(FileDescriptor fd, PrintWriter pw) {
415        synchronized (mLock) {
416            pw.println("  USB Host State:");
417            for (String name : mDevices.keySet()) {
418                pw.println("    " + name + ": " + mDevices.get(name));
419            }
420        }
421    }
422
423    private native void monitorUsbHostBus();
424    private native ParcelFileDescriptor nativeOpenDevice(String deviceName);
425}
426