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