Add support for uploading files via MTP.

Change-Id: Id1811ab70cb28be471e0a99999e9ad5380deac49
This commit is contained in:
Tomasz Mikolajewski
2015-08-24 16:10:51 +09:00
parent c61b3aabdd
commit b80a3cfd05
12 changed files with 269 additions and 47 deletions

View File

@@ -18115,7 +18115,7 @@ package android.mtp {
method public boolean importFile(int, java.lang.String);
method public boolean importFile(int, android.os.ParcelFileDescriptor);
method public boolean open(android.hardware.usb.UsbDeviceConnection);
method public boolean sendObject(int, android.os.ParcelFileDescriptor);
method public boolean sendObject(int, int, android.os.ParcelFileDescriptor);
method public android.mtp.MtpObjectInfo sendObjectInfo(android.mtp.MtpObjectInfo);
}

View File

@@ -19627,7 +19627,7 @@ package android.mtp {
method public boolean importFile(int, java.lang.String);
method public boolean importFile(int, android.os.ParcelFileDescriptor);
method public boolean open(android.hardware.usb.UsbDeviceConnection);
method public boolean sendObject(int, android.os.ParcelFileDescriptor);
method public boolean sendObject(int, int, android.os.ParcelFileDescriptor);
method public android.mtp.MtpObjectInfo sendObjectInfo(android.mtp.MtpObjectInfo);
}

View File

@@ -257,11 +257,12 @@ public final class MtpDevice {
* on completion, and must be done by the caller.
*
* @param objectHandle handle of the target file
* @param size size of the file in bytes
* @param descriptor file descriptor to read the data from.
* @return true if the file transfer succeeds
*/
public boolean sendObject(int objectHandle, ParcelFileDescriptor descriptor) {
return native_send_object(objectHandle, descriptor.getFd());
public boolean sendObject(int objectHandle, int size, ParcelFileDescriptor descriptor) {
return native_send_object(objectHandle, size, descriptor.getFd());
}
/**
@@ -294,6 +295,6 @@ public final class MtpDevice {
private native long native_get_storage_id(int objectHandle);
private native boolean native_import_file(int objectHandle, String destPath);
private native boolean native_import_file(int objectHandle, int fd);
private native boolean native_send_object(int objectHandle, int fd);
private native boolean native_send_object(int objectHandle, int size, int fd);
private native MtpObjectInfo native_send_object_info(MtpObjectInfo info);
}

View File

@@ -273,25 +273,25 @@ public final class MtpObjectInfo {
public Builder(MtpObjectInfo objectInfo) {
mObjectInfo = new MtpObjectInfo();
mObjectInfo.mHandle = -1;
mObjectInfo.mAssociationDesc = mObjectInfo.mAssociationDesc;
mObjectInfo.mAssociationType = mObjectInfo.mAssociationType;
mObjectInfo.mCompressedSize = mObjectInfo.mCompressedSize;
mObjectInfo.mDateCreated = mObjectInfo.mDateCreated;
mObjectInfo.mDateModified = mObjectInfo.mDateModified;
mObjectInfo.mFormat = mObjectInfo.mFormat;
mObjectInfo.mImagePixDepth = mObjectInfo.mImagePixDepth;
mObjectInfo.mImagePixHeight = mObjectInfo.mImagePixHeight;
mObjectInfo.mImagePixWidth = mObjectInfo.mImagePixWidth;
mObjectInfo.mKeywords = mObjectInfo.mKeywords;
mObjectInfo.mName = mObjectInfo.mName;
mObjectInfo.mParent = mObjectInfo.mParent;
mObjectInfo.mProtectionStatus = mObjectInfo.mProtectionStatus;
mObjectInfo.mSequenceNumber = mObjectInfo.mSequenceNumber;
mObjectInfo.mStorageId = mObjectInfo.mStorageId;
mObjectInfo.mThumbCompressedSize = mObjectInfo.mThumbCompressedSize;
mObjectInfo.mThumbFormat = mObjectInfo.mThumbFormat;
mObjectInfo.mThumbPixHeight = mObjectInfo.mThumbPixHeight;
mObjectInfo.mThumbPixWidth = mObjectInfo.mThumbPixWidth;
mObjectInfo.mAssociationDesc = objectInfo.mAssociationDesc;
mObjectInfo.mAssociationType = objectInfo.mAssociationType;
mObjectInfo.mCompressedSize = objectInfo.mCompressedSize;
mObjectInfo.mDateCreated = objectInfo.mDateCreated;
mObjectInfo.mDateModified = objectInfo.mDateModified;
mObjectInfo.mFormat = objectInfo.mFormat;
mObjectInfo.mImagePixDepth = objectInfo.mImagePixDepth;
mObjectInfo.mImagePixHeight = objectInfo.mImagePixHeight;
mObjectInfo.mImagePixWidth = objectInfo.mImagePixWidth;
mObjectInfo.mKeywords = objectInfo.mKeywords;
mObjectInfo.mName = objectInfo.mName;
mObjectInfo.mParent = objectInfo.mParent;
mObjectInfo.mProtectionStatus = objectInfo.mProtectionStatus;
mObjectInfo.mSequenceNumber = objectInfo.mSequenceNumber;
mObjectInfo.mStorageId = objectInfo.mStorageId;
mObjectInfo.mThumbCompressedSize = objectInfo.mThumbCompressedSize;
mObjectInfo.mThumbFormat = objectInfo.mThumbFormat;
mObjectInfo.mThumbPixHeight = objectInfo.mThumbPixHeight;
mObjectInfo.mThumbPixWidth = objectInfo.mThumbPixWidth;
}
public Builder setAssociationDesc(int value) {

View File

@@ -412,18 +412,13 @@ android_mtp_MtpDevice_import_file_to_fd(JNIEnv *env, jobject thiz, jint object_i
}
static jboolean
android_mtp_MtpDevice_send_object(JNIEnv *env, jobject thiz, jint object_id, jint fd)
android_mtp_MtpDevice_send_object(JNIEnv *env, jobject thiz, jint object_id, jint size, jint fd)
{
MtpDevice* device = get_device_from_object(env, thiz);
if (!device)
return JNI_FALSE;
MtpObjectInfo* object_info = device->getObjectInfo(object_id);
if (!object_info)
return JNI_FALSE;
bool result = device->sendObject(object_info, fd);
delete object_info;
return result;
return device->sendObject(object_id, size, fd);
}
static jobject
@@ -516,7 +511,7 @@ static JNINativeMethod gMethods[] = {
{"native_import_file", "(ILjava/lang/String;)Z",
(void *)android_mtp_MtpDevice_import_file},
{"native_import_file", "(II)Z",(void *)android_mtp_MtpDevice_import_file_to_fd},
{"native_send_object", "(II)Z",(void *)android_mtp_MtpDevice_send_object},
{"native_send_object", "(III)Z",(void *)android_mtp_MtpDevice_send_object},
{"native_send_object_info", "(Landroid/mtp/MtpObjectInfo;)Landroid/mtp/MtpObjectInfo;",
(void *)android_mtp_MtpDevice_send_object_info}
};

View File

@@ -76,18 +76,25 @@ class MtpDocument {
void addToCursor(Identifier rootIdentifier, MatrixCursor.RowBuilder builder) {
final Identifier identifier = new Identifier(
rootIdentifier.mDeviceId, rootIdentifier.mStorageId, mObjectHandle);
final String mimeType = formatTypeToMimeType(mFormat);
int flag = 0;
if (mObjectHandle != DUMMY_HANDLE_FOR_ROOT) {
flag |= DocumentsContract.Document.FLAG_SUPPORTS_DELETE;
if (mThumbSize > 0) {
flag |= DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL;
}
if (!mReadOnly) {
flag |= DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
DocumentsContract.Document.FLAG_SUPPORTS_WRITE;
}
}
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR && !mReadOnly) {
flag |= DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE;
}
builder.add(Document.COLUMN_DOCUMENT_ID, identifier.toDocumentId());
builder.add(Document.COLUMN_DISPLAY_NAME, mName);
builder.add(Document.COLUMN_MIME_TYPE, formatTypeToMimeType(mFormat));
builder.add(Document.COLUMN_MIME_TYPE, mimeType);
builder.add(
Document.COLUMN_LAST_MODIFIED,
mDateModified != null ? mDateModified.getTime() : null);

View File

@@ -167,13 +167,18 @@ public class MtpDocumentsProvider extends DocumentsProvider {
public ParcelFileDescriptor openDocument(
String documentId, String mode, CancellationSignal signal)
throws FileNotFoundException {
if (!"r".equals(mode) && !"w".equals(mode)) {
// TODO: Support seekable file.
throw new UnsupportedOperationException("The provider does not support seekable file.");
}
final Identifier identifier = Identifier.createFromDocumentId(documentId);
try {
return mPipeManager.readDocument(mMtpManager, identifier);
switch (mode) {
case "r":
return mPipeManager.readDocument(mMtpManager, identifier);
case "w":
return mPipeManager.writeDocument(getContext(), mMtpManager, identifier);
default:
// TODO: Add support for seekable files.
throw new UnsupportedOperationException(
"The provider does not support seekable file.");
}
} catch (IOException error) {
throw new FileNotFoundException(error.getMessage());
}

View File

@@ -108,6 +108,11 @@ class MtpManager {
return results;
}
synchronized MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException {
final MtpDevice device = getDevice(deviceId);
return device.getObjectInfo(objectHandle);
}
synchronized MtpDocument getDocument(int deviceId, int objectHandle) throws IOException {
final MtpDevice device = getDevice(deviceId);
return new MtpDocument(device.getObjectInfo(objectHandle));
@@ -137,15 +142,20 @@ class MtpManager {
}
}
// TODO: Remove this method.
synchronized int createDocument(int deviceId, int storageId, int parentObjectHandle,
String mimeType, String name) throws IOException {
final MtpDevice device = getDevice(deviceId);
final MtpObjectInfo objectInfo = new MtpObjectInfo.Builder()
.setName(name)
.setStorageId(storageId)
.setParent(parentObjectHandle)
.setFormat(MtpDocument.mimeTypeToFormatType(mimeType))
.build();
return createDocument(deviceId, objectInfo);
}
synchronized int createDocument(int deviceId, MtpObjectInfo objectInfo) throws IOException {
final MtpDevice device = getDevice(deviceId);
final MtpObjectInfo result = device.sendObjectInfo(objectInfo);
if (result == null) {
throw new IOException("Failed to create a document");
@@ -168,6 +178,13 @@ class MtpManager {
device.importFile(objectHandle, target);
}
synchronized void sendObject(int deviceId, int objectHandle, int size,
ParcelFileDescriptor source) throws IOException {
final MtpDevice device = getDevice(deviceId);
if (!device.sendObject(objectHandle, size, source))
throw new IOException("Failed to send a document");
}
private MtpDevice getDevice(int deviceId) throws IOException {
final MtpDevice device = mDevices.get(deviceId);
if (device == null) {

View File

@@ -16,10 +16,16 @@
package com.android.mtp;
import android.content.Context;
import android.mtp.MtpObjectInfo;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -40,6 +46,13 @@ class PipeManager {
return task.getReadingFileDescriptor();
}
ParcelFileDescriptor writeDocument(Context context, MtpManager model, Identifier identifier)
throws IOException {
final Task task = new WriteDocumentTask(context, model, identifier);
mExecutor.execute(task);
return task.getWritingFileDescriptor();
}
ParcelFileDescriptor readThumbnail(MtpManager model, Identifier identifier) throws IOException {
final Task task = new GetThumbnailTask(model, identifier);
mExecutor.execute(task);
@@ -60,6 +73,10 @@ class PipeManager {
ParcelFileDescriptor getReadingFileDescriptor() {
return mDescriptors[0];
}
ParcelFileDescriptor getWritingFileDescriptor() {
return mDescriptors[1];
}
}
private static class ImportFileTask extends Task {
@@ -83,6 +100,70 @@ class PipeManager {
}
}
private static class WriteDocumentTask extends Task {
private final Context mContext;
WriteDocumentTask(Context context, MtpManager model, Identifier identifier)
throws IOException {
super(model, identifier);
mContext = context;
}
@Override
public void run() {
File tempFile = null;
try {
// Obtain a temporary file and copy the data to it.
tempFile = mContext.getCacheDir().createTempFile("mtp", "tmp");
try (
final FileOutputStream tempOutputStream =
new ParcelFileDescriptor.AutoCloseOutputStream(
ParcelFileDescriptor.open(
tempFile, ParcelFileDescriptor.MODE_WRITE_ONLY));
final ParcelFileDescriptor.AutoCloseInputStream inputStream =
new ParcelFileDescriptor.AutoCloseInputStream(mDescriptors[0])
) {
final byte[] buffer = new byte[32 * 1024];
int bytes;
while ((bytes = inputStream.read(buffer)) != -1) {
mDescriptors[0].checkError();
tempOutputStream.write(buffer, 0, bytes);
}
tempOutputStream.flush();
}
// Get the placeholder object info.
final MtpObjectInfo placeholderObjectInfo =
mManager.getObjectInfo(mIdentifier.mDeviceId, mIdentifier.mObjectHandle);
// Delete the target object info if it already exists (as a placeholder).
mManager.deleteDocument(mIdentifier.mDeviceId, mIdentifier.mObjectHandle);
// Create the target object info with a correct file size.
final int targetObjectHandle =
mManager.createDocument(
mIdentifier.mDeviceId,
new MtpObjectInfo.Builder(placeholderObjectInfo)
.setCompressedSize((int) tempFile.length())
.build());
// Upload the object.
final ParcelFileDescriptor tempInputDescriptor = ParcelFileDescriptor.open(
tempFile, ParcelFileDescriptor.MODE_READ_ONLY);
mManager.sendObject(mIdentifier.mDeviceId,
targetObjectHandle, (int) tempFile.length(), tempInputDescriptor);
} catch (IOException error) {
Log.w(MtpDocumentsProvider.TAG,
"Failed to send a file because of: " + error.getMessage());
} finally {
if (tempFile != null) {
tempFile.delete();
}
}
}
}
private static class GetThumbnailTask extends Task {
GetThumbnailTask(MtpManager model, Identifier identifier) throws IOException {
super(model, identifier);

View File

@@ -211,7 +211,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
new Date(1422716400000L) /* modified date */,
1024 * 1024 * 5 /* file size */,
1024 * 50 /* thumbnail size */,
true /* read only */));
false /* read only */));
final Cursor cursor = mProvider.queryDocument("0_1_2", null);
assertEquals(1, cursor.getCount());
@@ -222,11 +222,37 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
assertEquals(1422716400000L, cursor.getLong(3));
assertEquals(
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL,
cursor.getInt(4));
assertEquals(1024 * 1024 * 5, cursor.getInt(5));
}
public void testQueryDocument_directory() throws IOException {
mMtpManager.setDocument(0, 2, new MtpDocument(
2 /* object handle */,
0x3001 /* directory */,
"directory" /* display name */,
new Date(1422716400000L) /* modified date */,
0 /* file size */,
0 /* thumbnail size */,
false /* read only */));
final Cursor cursor = mProvider.queryDocument("0_1_2", null);
assertEquals(1, cursor.getCount());
cursor.moveToNext();
assertEquals("0_1_2", cursor.getString(0));
assertEquals(DocumentsContract.Document.MIME_TYPE_DIR, cursor.getString(1));
assertEquals("directory", cursor.getString(2));
assertEquals(1422716400000L, cursor.getLong(3));
assertEquals(
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
DocumentsContract.Document.FLAG_SUPPORTS_WRITE |
DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE,
cursor.getInt(4));
assertEquals(0, cursor.getInt(5));
}
public void testQueryDocument_forRoot() throws IOException {
mMtpManager.setRoots(0, new MtpRoot[] {
new MtpRoot(
@@ -269,10 +295,7 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
assertEquals("image/jpeg", cursor.getString(1));
assertEquals("image.jpg", cursor.getString(2));
assertEquals(0, cursor.getLong(3));
assertEquals(
DocumentsContract.Document.FLAG_SUPPORTS_DELETE |
DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL,
cursor.getInt(4));
assertEquals(DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL, cursor.getInt(4));
assertEquals(1024 * 1024 * 5, cursor.getInt(5));
assertFalse(cursor.moveToNext());

View File

@@ -21,6 +21,7 @@ import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import java.io.IOException;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -53,6 +54,41 @@ public class PipeManagerTest extends AndroidTestCase {
assertDescriptorError(descriptor);
}
public void testWriteDocument_basic() throws Exception {
// Create a placeholder file which should be replaced by a real file later.
mtpManager.setDocument(0, 1, new MtpDocument(1, 0, "", new Date(), 0, 0, false));
// Upload testing bytes.
final ParcelFileDescriptor descriptor = pipeManager.writeDocument(
getContext(), mtpManager, new Identifier(0, 0, 1));
final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
new ParcelFileDescriptor.AutoCloseOutputStream(descriptor);
outputStream.write(HELLO_BYTES, 0, HELLO_BYTES.length);
outputStream.close();
executor.awaitTermination(1000, TimeUnit.MILLISECONDS);
// Check if the placeholder file is removed.
try {
final MtpDocument placeholderDocument = mtpManager.getDocument(0, 1);
fail(); // The placeholder file has not been deleted.
} catch (IOException e) {
// Expected error, as the file is gone.
}
// Confirm that the target file is created.
final MtpDocument targetDocument = mtpManager.getDocument(
0, TestMtpManager.CREATED_DOCUMENT_HANDLE);
assertTrue(targetDocument != null);
// Verify uploaded bytes.
final byte[] uploadedBytes = mtpManager.getImportFileBytes(
0, TestMtpManager.CREATED_DOCUMENT_HANDLE);
assertEquals(HELLO_BYTES.length, uploadedBytes.length);
for (int i = 0; i < HELLO_BYTES.length; i++) {
assertEquals(HELLO_BYTES[i], uploadedBytes[i]);
}
}
public void testReadThumbnail_basic() throws Exception {
mtpManager.setThumbnail(0, 1, HELLO_BYTES);
final ParcelFileDescriptor descriptor = pipeManager.readThumbnail(

View File

@@ -17,10 +17,13 @@
package com.android.mtp;
import android.content.Context;
import android.mtp.MtpObjectInfo;
import android.mtp.MtpObjectInfo.Builder;
import android.os.ParcelFileDescriptor;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -28,6 +31,8 @@ import java.util.Set;
import java.util.TreeSet;
public class TestMtpManager extends MtpManager {
public static final int CREATED_DOCUMENT_HANDLE = 1000;
protected static String pack(int... args) {
return Arrays.toString(args);
}
@@ -65,6 +70,10 @@ public class TestMtpManager extends MtpManager {
mImportFileBytes.put(pack(deviceId, objectHandle), bytes);
}
byte[] getImportFileBytes(int deviceId, int objectHandle) {
return mImportFileBytes.get(pack(deviceId, objectHandle));
}
void setThumbnail(int deviceId, int objectHandle, byte[] bytes) {
mThumbnailBytes.put(pack(deviceId, objectHandle), bytes);
}
@@ -108,6 +117,15 @@ public class TestMtpManager extends MtpManager {
}
}
@Override
MtpObjectInfo getObjectInfo(int deviceId, int objectHandle) throws IOException {
final MtpDocument document = getDocument(deviceId, objectHandle);
// It's impossible to set an object id of MtpObjectInfo at this stage. Also,
// it's hard to get any information from MtpDocument, as it's designed to return them
// only via cursors. Rework these.
return new MtpObjectInfo.Builder().build();
}
@Override
int[] getObjectHandles(int deviceId, int storageId, int parentObjectHandle) throws IOException {
final String key = pack(deviceId, storageId, parentObjectHandle);
@@ -119,8 +137,9 @@ public class TestMtpManager extends MtpManager {
}
@Override
void importFile(int deviceId, int storageId, ParcelFileDescriptor target) throws IOException {
final String key = pack(deviceId, storageId);
void importFile(int deviceId, int objectHandle, ParcelFileDescriptor target)
throws IOException {
final String key = pack(deviceId, objectHandle);
if (mImportFileBytes.containsKey(key)) {
try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
new ParcelFileDescriptor.AutoCloseOutputStream(target)) {
@@ -131,6 +150,44 @@ public class TestMtpManager extends MtpManager {
}
}
@Override
int createDocument(int deviceId, MtpObjectInfo objectInfo) throws IOException {
// For simplicity, it allows to create only one document, and it always has the hardcoded
// CREATED_DOCUMENT_HANDLE document handle.
final String key = pack(deviceId, CREATED_DOCUMENT_HANDLE);
if (!mDocuments.containsKey(key)) {
mDocuments.put(key, new MtpDocument(
CREATED_DOCUMENT_HANDLE,
objectInfo.getFormat(),
objectInfo.getName(),
new Date(objectInfo.getDateModified()),
objectInfo.getCompressedSize(),
objectInfo.getThumbCompressedSize(),
false /* Always writable for testing. */));
} else {
throw new IOException();
}
return CREATED_DOCUMENT_HANDLE;
}
@Override
void sendObject(int deviceId, int objectHandle, int size, ParcelFileDescriptor source)
throws IOException {
final String key = pack(deviceId, objectHandle);
if (!mDocuments.containsKey(key)) {
throw new IOException();
}
ParcelFileDescriptor.AutoCloseInputStream inputStream =
new ParcelFileDescriptor.AutoCloseInputStream(source);
byte[] buffer = new byte[size];
if (inputStream.read(buffer, 0, size) != size) {
throw new IOException();
}
mImportFileBytes.put(pack(deviceId, objectHandle), buffer);
}
@Override
byte[] getThumbnail(int deviceId, int objectHandle) throws IOException {
final String key = pack(deviceId, objectHandle);