Home | History | Annotate | Download | only in mtp
      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 
     72     MtpManager(Context context) {
     73         mManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
     74     }
     75 
     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 
    114     synchronized void closeDevice(int deviceId) throws IOException {
    115         getDevice(deviceId).close();
    116         mDevices.remove(deviceId);
    117     }
    118 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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.getCompressedSize(), source)) {
    204                     throw new IOException("Failed to send contents of a document");
    205                 }
    206             }
    207             return sendObjectInfoResult.getObjectHandle();
    208         }
    209     }
    210 
    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 
    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
    233     MtpEvent readEvent(int deviceId, CancellationSignal signal) throws IOException {
    234         final MtpDevice device = getDevice(deviceId);
    235         return device.readEvent(signal);
    236     }
    237 
    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 
    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 
    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 
    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 
    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 
    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