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)) {