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 
17 package com.android.gallery3d.data;
18 
19 import android.annotation.TargetApi;
20 import android.app.PendingIntent;
21 import android.content.BroadcastReceiver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.hardware.usb.UsbConstants;
26 import android.hardware.usb.UsbDevice;
27 import android.hardware.usb.UsbDeviceConnection;
28 import android.hardware.usb.UsbInterface;
29 import android.hardware.usb.UsbManager;
30 import android.mtp.MtpDevice;
31 import android.mtp.MtpObjectInfo;
32 import android.mtp.MtpStorageInfo;
33 import android.util.Log;
34 
35 import com.android.gallery3d.common.ApiHelper;
36 
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import 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)
47 public 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          */
deviceAdded(MtpDevice device)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          */
deviceRemoved(MtpDevice device)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      */
isCamera(UsbDevice device)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      */
MtpClient(Context context)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      */
openDeviceLocked(UsbDevice usbDevice)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      */
close()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      */
addListener(Listener listener)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      */
removeListener(Listener listener)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      */
getDevice(String deviceName)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      */
getDevice(int id)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      */
getDeviceList()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      */
getStorageList(String deviceName)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      */
getObjectInfo(String deviceName, int objectHandle)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      */
deleteObject(String deviceName, int objectHandle)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      */
getObjectList(String deviceName, int storageId, int objectHandle)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      */
getObject(String deviceName, int objectHandle, int objectSize)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      */
getThumbnail(String deviceName, int objectHandle)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      */
importFile(String deviceName, int objectHandle, String destPath)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