Add support for uploading files via MTP.
Change-Id: Id1811ab70cb28be471e0a99999e9ad5380deac49
diff --git a/api/current.txt b/api/current.txt
index df74b45..e9c5727 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -18115,7 +18115,7 @@
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);
}
diff --git a/api/system-current.txt b/api/system-current.txt
index bcae74b..e234970 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -19627,7 +19627,7 @@
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);
}
diff --git a/media/java/android/mtp/MtpDevice.java b/media/java/android/mtp/MtpDevice.java
index fbe047d..3cd157e 100644
--- a/media/java/android/mtp/MtpDevice.java
+++ b/media/java/android/mtp/MtpDevice.java
@@ -257,11 +257,12 @@
* 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 @@
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);
}
diff --git a/media/java/android/mtp/MtpObjectInfo.java b/media/java/android/mtp/MtpObjectInfo.java
index f79d52e..a080c73 100644
--- a/media/java/android/mtp/MtpObjectInfo.java
+++ b/media/java/android/mtp/MtpObjectInfo.java
@@ -273,25 +273,25 @@
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) {
diff --git a/media/jni/android_mtp_MtpDevice.cpp b/media/jni/android_mtp_MtpDevice.cpp
index 9dd3861..2a46ee7 100644
--- a/media/jni/android_mtp_MtpDevice.cpp
+++ b/media/jni/android_mtp_MtpDevice.cpp
@@ -412,18 +412,13 @@
}
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 @@
{"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}
};
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocument.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocument.java
index 7126694..c1d9609 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocument.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocument.java
@@ -76,18 +76,25 @@
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);
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
index 031cc07..a3cf3e2 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpDocumentsProvider.java
@@ -167,13 +167,18 @@
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());
}
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
index 27ba794..6647009 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/MtpManager.java
@@ -108,6 +108,11 @@
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 @@
}
}
+ // 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 @@
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) {
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java b/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java
index ba13b31..53f1b65 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/PipeManager.java
@@ -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 @@
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 @@
ParcelFileDescriptor getReadingFileDescriptor() {
return mDescriptors[0];
}
+
+ ParcelFileDescriptor getWritingFileDescriptor() {
+ return mDescriptors[1];
+ }
}
private static class ImportFileTask extends Task {
@@ -83,6 +100,70 @@
}
}
+ 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);
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
index 16be669..1826bd0 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/MtpDocumentsProviderTest.java
@@ -211,7 +211,7 @@
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 @@
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 @@
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());
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java
index 35918e1..e2cc3ed 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/PipeManagerTest.java
@@ -21,6 +21,7 @@
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 @@
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(
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java
index 40de7b4..94b5ba0 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/TestMtpManager.java
@@ -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.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 @@
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);
}
@@ -109,6 +118,15 @@
}
@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);
if (mObjectHandles.containsKey(key)) {
@@ -119,8 +137,9 @@
}
@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)) {
@@ -132,6 +151,44 @@
}
@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);
if (mThumbnailBytes.containsKey(key)) {