| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.mtp; |
| |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.hardware.usb.UsbConstants; |
| import android.hardware.usb.UsbDevice; |
| import android.hardware.usb.UsbDeviceConnection; |
| import android.hardware.usb.UsbInterface; |
| import android.hardware.usb.UsbManager; |
| import android.mtp.MtpConstants; |
| import android.mtp.MtpDevice; |
| import android.mtp.MtpDeviceInfo; |
| import android.mtp.MtpEvent; |
| import android.mtp.MtpObjectInfo; |
| import android.mtp.MtpStorageInfo; |
| import android.os.Build; |
| import android.os.CancellationSignal; |
| import android.os.ParcelFileDescriptor; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| |
| /** |
| * The model wrapping android.mtp API. |
| */ |
| class MtpManager { |
| final static int OBJECT_HANDLE_ROOT_CHILDREN = -1; |
| |
| /** |
| * Subclass for PTP. |
| */ |
| private static final int SUBCLASS_STILL_IMAGE_CAPTURE = 1; |
| |
| /** |
| * Subclass for Android style MTP. |
| */ |
| private static final int SUBCLASS_MTP = 0xff; |
| |
| /** |
| * Protocol for Picture Transfer Protocol (PIMA 15470). |
| */ |
| private static final int PROTOCOL_PICTURE_TRANSFER = 1; |
| |
| /** |
| * Protocol for Android style MTP. |
| */ |
| private static final int PROTOCOL_MTP = 0; |
| |
| private final UsbManager mManager; |
| private final SparseArray<MtpDevice> mDevices = new SparseArray<>(); |
| |
| MtpManager(Context context) { |
| mManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); |
| } |
| |
| synchronized MtpDeviceRecord openDevice(int deviceId) throws IOException { |
| UsbDevice rawDevice = null; |
| for (final UsbDevice candidate : mManager.getDeviceList().values()) { |
| if (candidate.getDeviceId() == deviceId) { |
| rawDevice = candidate; |
| break; |
| } |
| } |
| |
| ensureNotNull(rawDevice, "Not found USB device: " + deviceId); |
| |
| if (!mManager.hasPermission(rawDevice)) { |
| mManager.grantPermission(rawDevice); |
| if (!mManager.hasPermission(rawDevice)) { |
| throw new IOException("Failed to grant a device permission."); |
| } |
| } |
| |
| final MtpDevice device = new MtpDevice(rawDevice); |
| |
| final UsbDeviceConnection connection = ensureNotNull( |
| mManager.openDevice(rawDevice), |
| "Failed to open a USB connection."); |
| |
| if (!device.open(connection)) { |
| // We cannot open connection when another application use the device. |
| throw new BusyDeviceException(); |
| } |
| |
| // Handle devices that fail to obtain storages just after opening a MTP session. |
| final int[] storageIds = ensureNotNull( |
| device.getStorageIds(), |
| "Not found MTP storages in the device."); |
| |
| mDevices.put(deviceId, device); |
| |
| setInitVersion(rawDevice); |
| |
| return createDeviceRecord(rawDevice); |
| } |
| |
| synchronized void closeDevice(int deviceId) throws IOException { |
| getDevice(deviceId).close(); |
| mDevices.remove(deviceId); |
| } |
| |
| synchronized MtpDeviceRecord[] getDevices() { |
| final ArrayList<MtpDeviceRecord> devices = new ArrayList<>(); |
| for (UsbDevice device : mManager.getDeviceList().values()) { |
| if (!isMtpDevice(device)) { |
| continue; |
| } |
| devices.add(createDeviceRecord(device)); |
| } |
| return devices.toArray(new MtpDeviceRecord[devices.size()]); |
| } |
| |
| MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| return ensureNotNull( |
| device.getObjectInfo(objectHandle), |
| "Failed to get object info: " + objectHandle); |
| } |
| } |
| |
| int[] getObjectHandles(int deviceId, int storageId, int parentObjectHandle) |
| throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| return ensureNotNull( |
| device.getObjectHandles(storageId, 0 /* all format */, parentObjectHandle), |
| "Failed to fetch object handles."); |
| } |
| } |
| |
| byte[] getObject(int deviceId, int objectHandle, int expectedSize) |
| throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| return ensureNotNull( |
| device.getObject(objectHandle, expectedSize), |
| "Failed to fetch object bytes"); |
| } |
| } |
| |
| long getPartialObject(int deviceId, int objectHandle, long offset, long size, byte[] buffer) |
| throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| return device.getPartialObject(objectHandle, offset, size, buffer); |
| } |
| } |
| |
| long getPartialObject64(int deviceId, int objectHandle, long offset, long size, byte[] buffer) |
| throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| return device.getPartialObject64(objectHandle, offset, size, buffer); |
| } |
| } |
| |
| byte[] getThumbnail(int deviceId, int objectHandle) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| return ensureNotNull( |
| device.getThumbnail(objectHandle), |
| "Failed to obtain thumbnail bytes"); |
| } |
| } |
| |
| void deleteDocument(int deviceId, int objectHandle) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| if (!device.deleteObject(objectHandle)) { |
| throw new IOException("Failed to delete document"); |
| } |
| } |
| } |
| |
| int createDocument(int deviceId, MtpObjectInfo objectInfo, |
| ParcelFileDescriptor source) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| final MtpObjectInfo sendObjectInfoResult = device.sendObjectInfo(objectInfo); |
| if (sendObjectInfoResult == null) { |
| throw new SendObjectInfoFailure(); |
| } |
| if (objectInfo.getFormat() != MtpConstants.FORMAT_ASSOCIATION) { |
| if (!device.sendObject(sendObjectInfoResult.getObjectHandle(), |
| sendObjectInfoResult.getCompressedSizeLong(), source)) { |
| throw new IOException("Failed to send contents of a document"); |
| } |
| } |
| return sendObjectInfoResult.getObjectHandle(); |
| } |
| } |
| |
| int getParent(int deviceId, int objectHandle) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| final int result = (int) device.getParent(objectHandle); |
| if (result == 0xffffffff) { |
| throw new FileNotFoundException("Not found parent object"); |
| } |
| return result; |
| } |
| } |
| |
| void importFile(int deviceId, int objectHandle, ParcelFileDescriptor target) |
| throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| if (!device.importFile(objectHandle, target)) { |
| throw new IOException("Failed to import file to FD"); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| MtpEvent readEvent(int deviceId, CancellationSignal signal) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| return device.readEvent(signal); |
| } |
| |
| long getObjectSizeLong(int deviceId, int objectHandle, int format) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| return device.getObjectSizeLong(objectHandle, format); |
| } |
| |
| private synchronized MtpDevice getDevice(int deviceId) throws IOException { |
| return ensureNotNull( |
| mDevices.get(deviceId), |
| "USB device " + deviceId + " is not opened."); |
| } |
| |
| private MtpRoot[] getRoots(int deviceId) throws IOException { |
| final MtpDevice device = getDevice(deviceId); |
| synchronized (device) { |
| final int[] storageIds = |
| ensureNotNull(device.getStorageIds(), "Failed to obtain storage IDs."); |
| final ArrayList<MtpRoot> roots = new ArrayList<>(); |
| for (int i = 0; i < storageIds.length; i++) { |
| final MtpStorageInfo info = device.getStorageInfo(storageIds[i]); |
| if (info == null) { |
| continue; |
| } |
| roots.add(new MtpRoot(device.getDeviceId(), info)); |
| } |
| return roots.toArray(new MtpRoot[roots.size()]); |
| } |
| } |
| |
| private void setInitVersion(UsbDevice device) { |
| final MtpDevice mtpDevice = mDevices.get(device.getDeviceId()); |
| final boolean opened = mtpDevice != null; |
| final String name = device.getProductName(); |
| int[] devicePropertySupported = null; |
| |
| if (opened) { |
| final MtpDeviceInfo info = mtpDevice.getDeviceInfo(); |
| |
| if (info != null) { |
| devicePropertySupported = info.getDevicePropertySupported(); |
| |
| if (MtpDeviceRecord.isSupported(devicePropertySupported, |
| MtpConstants.DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO)) { |
| mtpDevice.setDevicePropertyInitVersion("Android/" |
| + Build.VERSION.RELEASE |
| + " Build/" + Build.VERSION.INCREMENTAL); |
| } |
| } |
| } |
| } |
| |
| private MtpDeviceRecord createDeviceRecord(UsbDevice device) { |
| final MtpDevice mtpDevice = mDevices.get(device.getDeviceId()); |
| final boolean opened = mtpDevice != null; |
| final String name = device.getProductName(); |
| MtpRoot[] roots; |
| int[] operationsSupported = null; |
| int[] eventsSupported = null; |
| int[] devicePropertySupported = null; |
| |
| if (opened) { |
| try { |
| roots = getRoots(device.getDeviceId()); |
| } catch (IOException exp) { |
| Log.e(MtpDocumentsProvider.TAG, "Failed to open device", exp); |
| // If we failed to fetch roots for the device, we still returns device model |
| // with an empty set of roots so that the device is shown DocumentsUI as long as |
| // the device is physically connected. |
| roots = new MtpRoot[0]; |
| } |
| final MtpDeviceInfo info = mtpDevice.getDeviceInfo(); |
| if (info != null) { |
| operationsSupported = info.getOperationsSupported(); |
| eventsSupported = info.getEventsSupported(); |
| } |
| } else { |
| roots = new MtpRoot[0]; |
| } |
| return new MtpDeviceRecord( |
| device.getDeviceId(), name, device.getSerialNumber(), opened, roots, |
| operationsSupported, eventsSupported); |
| } |
| |
| static boolean isMtpDevice(UsbDevice device) { |
| for (int i = 0; i < device.getInterfaceCount(); i++) { |
| final UsbInterface usbInterface = device.getInterface(i); |
| if ((usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_STILL_IMAGE && |
| usbInterface.getInterfaceSubclass() == SUBCLASS_STILL_IMAGE_CAPTURE && |
| usbInterface.getInterfaceProtocol() == PROTOCOL_PICTURE_TRANSFER)) { |
| return true; |
| } |
| if (usbInterface.getInterfaceClass() == UsbConstants.USB_SUBCLASS_VENDOR_SPEC && |
| usbInterface.getInterfaceSubclass() == SUBCLASS_MTP && |
| usbInterface.getInterfaceProtocol() == PROTOCOL_MTP && |
| "MTP".equals(usbInterface.getName())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static <T> T ensureNotNull(@Nullable T t, String errorMessage) throws IOException { |
| if (t != null) { |
| return t; |
| } else { |
| throw new IOException(errorMessage); |
| } |
| } |
| } |