1/*
2 * Copyright (C) 2010 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.camerabrowser;
18
19import android.app.PendingIntent;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.hardware.usb.UsbConstants;
25import android.hardware.usb.UsbDevice;
26import android.hardware.usb.UsbDeviceConnection;
27import android.hardware.usb.UsbInterface;
28import android.hardware.usb.UsbManager;
29import android.mtp.MtpDevice;
30import android.mtp.MtpDeviceInfo;
31import android.mtp.MtpObjectInfo;
32import android.mtp.MtpStorageInfo;
33import android.os.ParcelFileDescriptor;
34import android.util.Log;
35
36import java.io.IOException;
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.List;
40
41/**
42 * This class helps an application manage a list of connected MTP or PTP devices.
43 * It listens for MTP devices being attached and removed from the USB host bus
44 * and notifies the application when the MTP device list changes.
45 */
46public class MtpClient {
47
48    private static final String TAG = "MtpClient";
49
50    private static final String ACTION_USB_PERMISSION =
51            "android.mtp.MtpClient.action.USB_PERMISSION";
52
53    private final Context mContext;
54    private final UsbManager mUsbManager;
55    private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
56    // mDevices contains all MtpDevices that have been seen by our client,
57    // so we can inform when the device has been detached.
58    // mDevices is also used for synchronization in this class.
59    private final HashMap<String, MtpDevice> mDevices = new HashMap<String, MtpDevice>();
60
61    private final PendingIntent mPermissionIntent;
62
63    private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
64        @Override
65        public void onReceive(Context context, Intent intent) {
66            String action = intent.getAction();
67            UsbDevice usbDevice = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
68            String deviceName = usbDevice.getDeviceName();
69
70            synchronized (mDevices) {
71                MtpDevice mtpDevice = mDevices.get(deviceName);
72
73                if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
74                    if (mtpDevice == null) {
75                        mtpDevice = openDeviceLocked(usbDevice);
76                    }
77                    if (mtpDevice != null) {
78                        for (Listener listener : mListeners) {
79                            listener.deviceAdded(mtpDevice);
80                        }
81                    }
82                } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
83                    if (mtpDevice != null) {
84                        mDevices.remove(deviceName);
85                        for (Listener listener : mListeners) {
86                            listener.deviceRemoved(mtpDevice);
87                        }
88                    }
89                } else if (ACTION_USB_PERMISSION.equals(action)) {
90                    boolean permission = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,
91                            false);
92                    Log.d(TAG, "ACTION_USB_PERMISSION: " + permission);
93                    if (permission) {
94                        if (mtpDevice == null) {
95                            mtpDevice = openDeviceLocked(usbDevice);
96                        }
97                        if (mtpDevice != null) {
98                            for (Listener listener : mListeners) {
99                                listener.deviceAdded(mtpDevice);
100                            }
101                        }
102                    }
103                }
104            }
105        }
106    };
107
108    /**
109     * An interface for being notified when MTP or PTP devices are attached
110     * or removed.  In the current implementation, only PTP devices are supported.
111     */
112    public interface Listener {
113        /**
114         * Called when a new device has been added
115         *
116         * @param device the new device that was added
117         */
118        public void deviceAdded(MtpDevice device);
119
120        /**
121         * Called when a new device has been removed
122         *
123         * @param device the device that was removed
124         */
125        public void deviceRemoved(MtpDevice device);
126    }
127
128    /**
129     * Tests to see if a {@link android.hardware.usb.UsbDevice}
130     * supports the PTP protocol (typically used by digital cameras)
131     *
132     * @param device the device to test
133     * @return true if the device is a PTP device.
134     */
135    static public boolean isCamera(UsbDevice device) {
136        int count = device.getInterfaceCount();
137        for (int i = 0; i < count; i++) {
138            UsbInterface intf = device.getInterface(i);
139            if (intf.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE &&
140                    intf.getInterfaceSubclass() == 1 &&
141                    intf.getInterfaceProtocol() == 1) {
142                return true;
143            }
144        }
145        return false;
146    }
147
148    /**
149     * MtpClient constructor
150     *
151     * @param context the {@link android.content.Context} to use for the MtpClient
152     */
153    public MtpClient(Context context) {
154        mContext = context;
155        mUsbManager = (UsbManager)context.getSystemService(Context.USB_SERVICE);
156        mPermissionIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(ACTION_USB_PERMISSION), 0);
157        IntentFilter filter = new IntentFilter();
158        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
159        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
160        filter.addAction(ACTION_USB_PERMISSION);
161        context.registerReceiver(mUsbReceiver, filter);
162    }
163
164    /**
165     * Opens the {@link android.hardware.usb.UsbDevice} for an MTP or PTP
166     * device and return an {@link android.mtp.MtpDevice} for it.
167     *
168     * @param device the device to open
169     * @return an MtpDevice for the device.
170     */
171    private MtpDevice openDeviceLocked(UsbDevice usbDevice) {
172        if (isCamera(usbDevice)) {
173            if (!mUsbManager.hasPermission(usbDevice)) {
174                mUsbManager.requestPermission(usbDevice, mPermissionIntent);
175            } else {
176                UsbDeviceConnection connection = mUsbManager.openDevice(usbDevice);
177                if (connection != null) {
178                    MtpDevice mtpDevice = new MtpDevice(usbDevice);
179                    if (mtpDevice.open(connection)) {
180                        mDevices.put(usbDevice.getDeviceName(), mtpDevice);
181                        return mtpDevice;
182                    }
183                }
184            }
185        }
186        return null;
187    }
188
189    /**
190     * Closes all resources related to the MtpClient object
191     */
192    public void close() {
193        mContext.unregisterReceiver(mUsbReceiver);
194    }
195
196    /**
197     * Registers a {@link android.mtp.MtpClient.Listener} interface to receive
198     * notifications when MTP or PTP devices are added or removed.
199     *
200     * @param listener the listener to register
201     */
202    public void addListener(Listener listener) {
203        synchronized (mDevices) {
204            if (!mListeners.contains(listener)) {
205                mListeners.add(listener);
206            }
207        }
208    }
209
210    /**
211     * Unregisters a {@link android.mtp.MtpClient.Listener} interface.
212     *
213     * @param listener the listener to unregister
214     */
215    public void removeListener(Listener listener) {
216        synchronized (mDevices) {
217            mListeners.remove(listener);
218        }
219    }
220
221    /**
222     * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
223     * with the given name.
224     *
225     * @param deviceName the name of the USB device
226     * @return the MtpDevice, or null if it does not exist
227     */
228    public MtpDevice getDevice(String deviceName) {
229        synchronized (mDevices) {
230            return mDevices.get(deviceName);
231        }
232    }
233
234    /**
235     * Retrieves an {@link android.mtp.MtpDevice} object for the USB device
236     * with the given ID.
237     *
238     * @param id the ID of the USB device
239     * @return the MtpDevice, or null if it does not exist
240     */
241    public MtpDevice getDevice(int id) {
242        synchronized (mDevices) {
243            return mDevices.get(UsbDevice.getDeviceName(id));
244        }
245    }
246
247    /**
248     * Retrieves a list of all currently connected {@link android.mtp.MtpDevice}.
249     *
250     * @return the list of MtpDevices
251     */
252    public List<MtpDevice> getDeviceList() {
253        synchronized (mDevices) {
254            // Query the USB manager since devices might have attached
255            // before we added our listener.
256            for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
257                if (mDevices.get(usbDevice.getDeviceName()) == null) {
258                    openDeviceLocked(usbDevice);
259                }
260            }
261
262            return new ArrayList<MtpDevice>(mDevices.values());
263        }
264    }
265
266    /**
267     * Retrieves a list of all {@link android.mtp.MtpStorageInfo}
268     * for the MTP or PTP device with the given USB device name
269     *
270     * @param deviceName the name of the USB device
271     * @return the list of MtpStorageInfo
272     */
273    public List<MtpStorageInfo> getStorageList(String deviceName) {
274        MtpDevice device = getDevice(deviceName);
275        if (device == null) {
276            return null;
277        }
278        int[] storageIds = device.getStorageIds();
279        if (storageIds == null) {
280            return null;
281        }
282
283        int length = storageIds.length;
284        ArrayList<MtpStorageInfo> storageList = new ArrayList<MtpStorageInfo>(length);
285        for (int i = 0; i < length; i++) {
286            MtpStorageInfo info = device.getStorageInfo(storageIds[i]);
287            if (info == null) {
288                Log.w(TAG, "getStorageInfo failed");
289            } else {
290                storageList.add(info);
291            }
292        }
293        return storageList;
294    }
295
296    /**
297     * Retrieves the {@link android.mtp.MtpObjectInfo} for an object on
298     * the MTP or PTP device with the given USB device name with the given
299     * object handle
300     *
301     * @param deviceName the name of the USB device
302     * @param objectHandle handle of the object to query
303     * @return the MtpObjectInfo
304     */
305    public MtpObjectInfo getObjectInfo(String deviceName, int objectHandle) {
306        MtpDevice device = getDevice(deviceName);
307        if (device == null) {
308            return null;
309        }
310        return device.getObjectInfo(objectHandle);
311    }
312
313    /**
314     * Deletes an object on the MTP or PTP device with the given USB device name.
315     *
316     * @param deviceName the name of the USB device
317     * @param objectHandle handle of the object to delete
318     * @return true if the deletion succeeds
319     */
320    public boolean deleteObject(String deviceName, int objectHandle) {
321        MtpDevice device = getDevice(deviceName);
322        if (device == null) {
323            return false;
324        }
325        return device.deleteObject(objectHandle);
326    }
327
328    /**
329     * Retrieves a list of {@link android.mtp.MtpObjectInfo} for all objects
330     * on the MTP or PTP device with the given USB device name and given storage ID
331     * and/or object handle.
332     * If the object handle is zero, then all objects in the root of the storage unit
333     * will be returned. Otherwise, all immediate children of the object will be returned.
334     * If the storage ID is also zero, then all objects on all storage units will be returned.
335     *
336     * @param deviceName the name of the USB device
337     * @param storageId the ID of the storage unit to query, or zero for all
338     * @param objectHandle the handle of the parent object to query, or zero for the storage root
339     * @return the list of MtpObjectInfo
340     */
341    public List<MtpObjectInfo> getObjectList(String deviceName, int storageId, int objectHandle) {
342        MtpDevice device = getDevice(deviceName);
343        if (device == null) {
344            return null;
345        }
346        if (objectHandle == 0) {
347            // all objects in root of storage
348            objectHandle = 0xFFFFFFFF;
349        }
350        int[] handles = device.getObjectHandles(storageId, 0, objectHandle);
351        if (handles == null) {
352            return null;
353        }
354
355        int length = handles.length;
356        ArrayList<MtpObjectInfo> objectList = new ArrayList<MtpObjectInfo>(length);
357        for (int i = 0; i < length; i++) {
358            MtpObjectInfo info = device.getObjectInfo(handles[i]);
359            if (info == null) {
360                Log.w(TAG, "getObjectInfo failed");
361            } else {
362                objectList.add(info);
363            }
364        }
365        return objectList;
366    }
367
368    /**
369     * Returns the data for an object as a byte array.
370     *
371     * @param deviceName the name of the USB device containing the object
372     * @param objectHandle handle of the object to read
373     * @param objectSize the size of the object (this should match
374     *      {@link android.mtp.MtpObjectInfo#getCompressedSize}
375     * @return the object's data, or null if reading fails
376     */
377    public byte[] getObject(String deviceName, int objectHandle, int objectSize) {
378        MtpDevice device = getDevice(deviceName);
379        if (device == null) {
380            return null;
381        }
382        return device.getObject(objectHandle, objectSize);
383    }
384
385    /**
386     * Returns the thumbnail data for an object as a byte array.
387     *
388     * @param deviceName the name of the USB device containing the object
389     * @param objectHandle handle of the object to read
390     * @return the object's thumbnail, or null if reading fails
391     */
392    public byte[] getThumbnail(String deviceName, int objectHandle) {
393        MtpDevice device = getDevice(deviceName);
394        if (device == null) {
395            return null;
396        }
397        return device.getThumbnail(objectHandle);
398    }
399
400    /**
401     * Copies the data for an object to a file in external storage.
402     *
403     * @param deviceName the name of the USB device containing the object
404     * @param objectHandle handle of the object to read
405     * @param destPath path to destination for the file transfer.
406     *      This path should be in the external storage as defined by
407     *      {@link android.os.Environment#getExternalStorageDirectory}
408     * @return true if the file transfer succeeds
409     */
410    public boolean importFile(String deviceName, int objectHandle, String destPath) {
411        MtpDevice device = getDevice(deviceName);
412        if (device == null) {
413            return false;
414        }
415        return device.importFile(objectHandle, destPath);
416    }
417}
418