1 /*
2  * Copyright (C) 2015 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.mtp;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.hardware.usb.UsbConstants;
22 import android.hardware.usb.UsbDevice;
23 import android.hardware.usb.UsbDeviceConnection;
24 import android.hardware.usb.UsbInterface;
25 import android.hardware.usb.UsbManager;
26 import android.mtp.MtpConstants;
27 import android.mtp.MtpDevice;
28 import android.mtp.MtpDeviceInfo;
29 import android.mtp.MtpEvent;
30 import android.mtp.MtpObjectInfo;
31 import android.mtp.MtpStorageInfo;
32 import android.os.CancellationSignal;
33 import android.os.ParcelFileDescriptor;
34 import android.util.Log;
35 import android.util.SparseArray;
36 
37 import com.android.internal.annotations.VisibleForTesting;
38 
39 import java.io.FileNotFoundException;
40 import java.io.IOException;
41 import java.util.ArrayList;
42 
43 /**
44  * The model wrapping android.mtp API.
45  */
46 class MtpManager {
47     final static int OBJECT_HANDLE_ROOT_CHILDREN = -1;
48 
49     /**
50      * Subclass for PTP.
51      */
52     private static final int SUBCLASS_STILL_IMAGE_CAPTURE = 1;
53 
54     /**
55      * Subclass for Android style MTP.
56      */
57     private static final int SUBCLASS_MTP = 0xff;
58 
59     /**
60      * Protocol for Picture Transfer Protocol (PIMA 15470).
61      */
62     private static final int PROTOCOL_PICTURE_TRANSFER = 1;
63 
64     /**
65      * Protocol for Android style MTP.
66      */
67     private static final int PROTOCOL_MTP = 0;
68 
69     private final UsbManager mManager;
70     private final SparseArray<MtpDevice> mDevices = new SparseArray<>();
71 
MtpManager(Context context)72     MtpManager(Context context) {
73         mManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
74     }
75 
openDevice(int deviceId)76     synchronized MtpDeviceRecord openDevice(int deviceId) throws IOException {
77         UsbDevice rawDevice = null;
78         for (final UsbDevice candidate : mManager.getDeviceList().values()) {
79             if (candidate.getDeviceId() == deviceId) {
80                 rawDevice = candidate;
81                 break;
82             }
83         }
84 
85         ensureNotNull(rawDevice, "Not found USB device: " + deviceId);
86 
87         if (!mManager.hasPermission(rawDevice)) {
88             mManager.grantPermission(rawDevice);
89             if (!mManager.hasPermission(rawDevice)) {
90                 throw new IOException("Failed to grant a device permission.");
91             }
92         }
93 
94         final MtpDevice device = new MtpDevice(rawDevice);
95 
96         final UsbDeviceConnection connection = ensureNotNull(
97                 mManager.openDevice(rawDevice),
98                 "Failed to open a USB connection.");
99 
100         if (!device.open(connection)) {
101             // We cannot open connection when another application use the device.
102             throw new BusyDeviceException();
103         }
104 
105         // Handle devices that fail to obtain storages just after opening a MTP session.
106         final int[] storageIds = ensureNotNull(
107                 device.getStorageIds(),
108                 "Not found MTP storages in the device.");
109 
110         mDevices.put(deviceId, device);
111         return createDeviceRecord(rawDevice);
112     }
113 
closeDevice(int deviceId)114     synchronized void closeDevice(int deviceId) throws IOException {
115         getDevice(deviceId).close();
116         mDevices.remove(deviceId);
117     }
118 
getDevices()119     synchronized MtpDeviceRecord[] getDevices() {
120         final ArrayList<MtpDeviceRecord> devices = new ArrayList<>();
121         for (UsbDevice device : mManager.getDeviceList().values()) {
122             if (!isMtpDevice(device)) {
123                 continue;
124             }
125             devices.add(createDeviceRecord(device));
126         }
127         return devices.toArray(new MtpDeviceRecord[devices.size()]);
128     }
129 
getObjectInfo(int deviceId, int objectHandle)130     MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException {
131         final MtpDevice device = getDevice(deviceId);
132         synchronized (device) {
133             return ensureNotNull(
134                     device.getObjectInfo(objectHandle),
135                     "Failed to get object info: " + objectHandle);
136         }
137     }
138 
getObjectHandles(int deviceId, int storageId, int parentObjectHandle)139     int[] getObjectHandles(int deviceId, int storageId, int parentObjectHandle)
140             throws IOException {
141         final MtpDevice device = getDevice(deviceId);
142         synchronized (device) {
143             return ensureNotNull(
144                     device.getObjectHandles(storageId, 0 /* all format */, parentObjectHandle),
145                     "Failed to fetch object handles.");
146         }
147     }
148 
getObject(int deviceId, int objectHandle, int expectedSize)149     byte[] getObject(int deviceId, int objectHandle, int expectedSize)
150             throws IOException {
151         final MtpDevice device = getDevice(deviceId);
152         synchronized (device) {
153             return ensureNotNull(
154                     device.getObject(objectHandle, expectedSize),
155                     "Failed to fetch object bytes");
156         }
157     }
158 
getPartialObject(int deviceId, int objectHandle, long offset, long size, byte[] buffer)159     long getPartialObject(int deviceId, int objectHandle, long offset, long size, byte[] buffer)
160             throws IOException {
161         final MtpDevice device = getDevice(deviceId);
162         synchronized (device) {
163             return device.getPartialObject(objectHandle, offset, size, buffer);
164         }
165     }
166 
getPartialObject64(int deviceId, int objectHandle, long offset, long size, byte[] buffer)167     long getPartialObject64(int deviceId, int objectHandle, long offset, long size, byte[] buffer)
168             throws IOException {
169         final MtpDevice device = getDevice(deviceId);
170         synchronized (device) {
171             return device.getPartialObject64(objectHandle, offset, size, buffer);
172         }
173     }
174 
getThumbnail(int deviceId, int objectHandle)175     byte[] getThumbnail(int deviceId, int objectHandle) throws IOException {
176         final MtpDevice device = getDevice(deviceId);
177         synchronized (device) {
178             return ensureNotNull(
179                     device.getThumbnail(objectHandle),
180                     "Failed to obtain thumbnail bytes");
181         }
182     }
183 
deleteDocument(int deviceId, int objectHandle)184     void deleteDocument(int deviceId, int objectHandle) throws IOException {
185         final MtpDevice device = getDevice(deviceId);
186         synchronized (device) {
187             if (!device.deleteObject(objectHandle)) {
188                 throw new IOException("Failed to delete document");
189             }
190         }
191     }
192 
createDocument(int deviceId, MtpObjectInfo objectInfo, ParcelFileDescriptor source)193     int createDocument(int deviceId, MtpObjectInfo objectInfo,
194             ParcelFileDescriptor source) throws IOException {
195         final MtpDevice device = getDevice(deviceId);
196         synchronized (device) {
197             final MtpObjectInfo sendObjectInfoResult = device.sendObjectInfo(objectInfo);
198             if (sendObjectInfoResult == null) {
199                 throw new SendObjectInfoFailure();
200             }
201             if (objectInfo.getFormat() != MtpConstants.FORMAT_ASSOCIATION) {
202                 if (!device.sendObject(sendObjectInfoResult.getObjectHandle(),
203                         sendObjectInfoResult.getCompressedSizeLong(), source)) {
204                     throw new IOException("Failed to send contents of a document");
205                 }
206             }
207             return sendObjectInfoResult.getObjectHandle();
208         }
209     }
210 
getParent(int deviceId, int objectHandle)211     int getParent(int deviceId, int objectHandle) throws IOException {
212         final MtpDevice device = getDevice(deviceId);
213         synchronized (device) {
214             final int result = (int) device.getParent(objectHandle);
215             if (result == 0xffffffff) {
216                 throw new FileNotFoundException("Not found parent object");
217             }
218             return result;
219         }
220     }
221 
importFile(int deviceId, int objectHandle, ParcelFileDescriptor target)222     void importFile(int deviceId, int objectHandle, ParcelFileDescriptor target)
223             throws IOException {
224         final MtpDevice device = getDevice(deviceId);
225         synchronized (device) {
226             if (!device.importFile(objectHandle, target)) {
227                 throw new IOException("Failed to import file to FD");
228             }
229         }
230     }
231 
232     @VisibleForTesting
readEvent(int deviceId, CancellationSignal signal)233     MtpEvent readEvent(int deviceId, CancellationSignal signal) throws IOException {
234         final MtpDevice device = getDevice(deviceId);
235         return device.readEvent(signal);
236     }
237 
getObjectSizeLong(int deviceId, int objectHandle, int format)238     long getObjectSizeLong(int deviceId, int objectHandle, int format) throws IOException {
239         final MtpDevice device = getDevice(deviceId);
240         return device.getObjectSizeLong(objectHandle, format);
241     }
242 
getDevice(int deviceId)243     private synchronized MtpDevice getDevice(int deviceId) throws IOException {
244         return ensureNotNull(
245                 mDevices.get(deviceId),
246                 "USB device " + deviceId + " is not opened.");
247     }
248 
getRoots(int deviceId)249     private MtpRoot[] getRoots(int deviceId) throws IOException {
250         final MtpDevice device = getDevice(deviceId);
251         synchronized (device) {
252             final int[] storageIds =
253                     ensureNotNull(device.getStorageIds(), "Failed to obtain storage IDs.");
254             final ArrayList<MtpRoot> roots = new ArrayList<>();
255             for (int i = 0; i < storageIds.length; i++) {
256                 final MtpStorageInfo info = device.getStorageInfo(storageIds[i]);
257                 if (info == null) {
258                     continue;
259                 }
260                 roots.add(new MtpRoot(device.getDeviceId(), info));
261             }
262             return roots.toArray(new MtpRoot[roots.size()]);
263         }
264     }
265 
createDeviceRecord(UsbDevice device)266     private MtpDeviceRecord createDeviceRecord(UsbDevice device) {
267         final MtpDevice mtpDevice = mDevices.get(device.getDeviceId());
268         final boolean opened = mtpDevice != null;
269         final String name = device.getProductName();
270         MtpRoot[] roots;
271         int[] operationsSupported = null;
272         int[] eventsSupported = null;
273         if (opened) {
274             try {
275                 roots = getRoots(device.getDeviceId());
276             } catch (IOException exp) {
277                 Log.e(MtpDocumentsProvider.TAG, "Failed to open device", exp);
278                 // If we failed to fetch roots for the device, we still returns device model
279                 // with an empty set of roots so that the device is shown DocumentsUI as long as
280                 // the device is physically connected.
281                 roots = new MtpRoot[0];
282             }
283             final MtpDeviceInfo info = mtpDevice.getDeviceInfo();
284             if (info != null) {
285                 operationsSupported = info.getOperationsSupported();
286                 eventsSupported = info.getEventsSupported();
287             }
288         } else {
289             roots = new MtpRoot[0];
290         }
291         return new MtpDeviceRecord(
292                 device.getDeviceId(), name, device.getSerialNumber(), opened, roots,
293                 operationsSupported, eventsSupported);
294     }
295 
isMtpDevice(UsbDevice device)296     static boolean isMtpDevice(UsbDevice device) {
297         for (int i = 0; i < device.getInterfaceCount(); i++) {
298             final UsbInterface usbInterface = device.getInterface(i);
299             if ((usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE &&
300                     usbInterface.getInterfaceSubclass() == SUBCLASS_STILL_IMAGE_CAPTURE &&
301                     usbInterface.getInterfaceProtocol() == PROTOCOL_PICTURE_TRANSFER)) {
302                 return true;
303             }
304             if (usbInterface.getInterfaceClass() == UsbConstants.USB_SUBCLASS_VENDOR_SPEC &&
305                     usbInterface.getInterfaceSubclass() == SUBCLASS_MTP &&
306                     usbInterface.getInterfaceProtocol() == PROTOCOL_MTP &&
307                     "MTP".equals(usbInterface.getName())) {
308                 return true;
309             }
310         }
311         return false;
312     }
313 
ensureNotNull(@ullable T t, String errorMessage)314     private static <T> T ensureNotNull(@Nullable T t, String errorMessage) throws IOException {
315         if (t != null) {
316             return t;
317         } else {
318             throw new IOException(errorMessage);
319         }
320     }
321 }
322