MTP: Add extended operations to support in-place editing of files

MTP does not support partial writes of files (the entire file must be transferred at once).
This makes it impossible to implement a FUSE file system for MTP
with acceptable performance.
To fix this problem, this change adds extended MTP operations to allow
partial writes to files:

SendPartialObject - allows writing a subset of a file, or appending to the end of a file

TruncateObject - allows changing the size of a file

BeginEditObject - must be called before using SendPartialObject and TruncateObject

EndEditObject - commits changes to a file after it has been edited with SendPartialObject or TruncateObject

We also add GetPartialObject64, which is the same as GetPartialObject
but has a 64 bit offset rather than 32.

Change-Id: I4b110748b97ae05cdc8aab02ecdbbbeb263f7840
Signed-off-by: Mike Lockwood <lockwood@android.com>
diff --git a/media/mtp/MtpServer.cpp b/media/mtp/MtpServer.cpp
index 37e02a3..ff4009c 100644
--- a/media/mtp/MtpServer.cpp
+++ b/media/mtp/MtpServer.cpp
@@ -30,6 +30,7 @@
 
 #include "MtpDebug.h"
 #include "MtpDatabase.h"
+#include "MtpObjectInfo.h"
 #include "MtpProperty.h"
 #include "MtpServer.h"
 #include "MtpStorage.h"
@@ -79,6 +80,12 @@
     MTP_OPERATION_GET_OBJECT_REFERENCES,
     MTP_OPERATION_SET_OBJECT_REFERENCES,
 //    MTP_OPERATION_SKIP,
+    // Android extension for direct file IO
+    MTP_OPERATION_GET_PARTIAL_OBJECT_64,
+    MTP_OPERATION_SEND_PARTIAL_OBJECT,
+    MTP_OPERATION_TRUNCATE_OBJECT,
+    MTP_OPERATION_BEGIN_EDIT_OBJECT,
+    MTP_OPERATION_END_EDIT_OBJECT,
 };
 
 static const MtpEventCode kSupportedEventCodes[] = {
@@ -218,6 +225,15 @@
         }
     }
 
+    // commit any open edits
+    int count = mObjectEditList.size();
+    for (int i = 0; i < count; i++) {
+        ObjectEdit* edit = mObjectEditList[i];
+        commitEdit(edit);
+        delete edit;
+    }
+    mObjectEditList.clear();
+
     if (mSessionOpen)
         mDatabase->sessionEnded();
 }
@@ -252,6 +268,44 @@
     }
 }
 
+void MtpServer::addEditObject(MtpObjectHandle handle, MtpString& path,
+        uint64_t size, MtpObjectFormat format, int fd) {
+    ObjectEdit*  edit = new ObjectEdit;
+    edit->handle = handle;
+    edit->path = path;
+    edit->size = size;
+    edit->format = format;
+    edit->fd = fd;
+    mObjectEditList.add(edit);
+}
+
+MtpServer::ObjectEdit* MtpServer::getEditObject(MtpObjectHandle handle) {
+    int count = mObjectEditList.size();
+    for (int i = 0; i < count; i++) {
+        ObjectEdit* edit = mObjectEditList[i];
+        if (edit->handle == handle) return edit;
+    }
+    return NULL;
+}
+
+void MtpServer::removeEditObject(MtpObjectHandle handle) {
+    int count = mObjectEditList.size();
+    for (int i = 0; i < count; i++) {
+        ObjectEdit* edit = mObjectEditList[i];
+        if (edit->handle == handle) {
+            delete edit;
+            mObjectEditList.removeAt(i);
+            return;
+        }
+    }
+    LOGE("ObjectEdit not found in removeEditObject");
+}
+
+void MtpServer::commitEdit(ObjectEdit* edit) {
+    mDatabase->endSendObject((const char *)edit->path, edit->handle, edit->format, true);
+}
+
+
 bool MtpServer::handleRequest() {
     Mutex::Autolock autoLock(mMutex);
 
@@ -322,7 +376,8 @@
             response = doGetObject();
             break;
         case MTP_OPERATION_GET_PARTIAL_OBJECT:
-            response = doGetPartialObject();
+        case MTP_OPERATION_GET_PARTIAL_OBJECT_64:
+            response = doGetPartialObject(operation);
             break;
         case MTP_OPERATION_SEND_OBJECT_INFO:
             response = doSendObjectInfo();
@@ -339,6 +394,18 @@
         case MTP_OPERATION_GET_DEVICE_PROP_DESC:
             response = doGetDevicePropDesc();
             break;
+        case MTP_OPERATION_SEND_PARTIAL_OBJECT:
+            response = doSendPartialObject();
+            break;
+        case MTP_OPERATION_TRUNCATE_OBJECT:
+            response = doTruncateObject();
+            break;
+        case MTP_OPERATION_BEGIN_EDIT_OBJECT:
+            response = doBeginEditObject();
+            break;
+        case MTP_OPERATION_END_EDIT_OBJECT:
+            response = doEndEditObject();
+            break;
         default:
             LOGE("got unsupported command %s", MtpDebug::getOperationCodeName(operation));
             response = MTP_RESPONSE_OPERATION_NOT_SUPPORTED;
@@ -363,7 +430,7 @@
     mData.putUInt16(MTP_STANDARD_VERSION);
     mData.putUInt32(6); // MTP Vendor Extension ID
     mData.putUInt16(MTP_STANDARD_VERSION);
-    string.set("microsoft.com: 1.0;");
+    string.set("microsoft.com: 1.0; android.com: 1.0;");
     mData.putString(string); // MTP Extensions
     mData.putUInt16(0); //Functional Mode
     mData.putAUInt16(kSupportedOperationCodes,
@@ -601,7 +668,40 @@
     if (!hasStorage())
         return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
     MtpObjectHandle handle = mRequest.getParameter(1);
-    return mDatabase->getObjectInfo(handle, mData);
+    MtpObjectInfo info(handle);
+    MtpResponseCode result = mDatabase->getObjectInfo(handle, info);
+    if (result == MTP_RESPONSE_OK) {
+        char    date[20];
+
+        mData.putUInt32(info.mStorageID);
+        mData.putUInt16(info.mFormat);
+        mData.putUInt16(info.mProtectionStatus);
+
+        // if object is being edited the database size may be out of date
+        uint32_t size = info.mCompressedSize;
+        ObjectEdit* edit = getEditObject(handle);
+        if (edit)
+            size = (edit->size > 0xFFFFFFFFLL ? 0xFFFFFFFF : (uint32_t)edit->size);
+        mData.putUInt32(size);
+
+        mData.putUInt16(info.mThumbFormat);
+        mData.putUInt32(info.mThumbCompressedSize);
+        mData.putUInt32(info.mThumbPixWidth);
+        mData.putUInt32(info.mThumbPixHeight);
+        mData.putUInt32(info.mImagePixWidth);
+        mData.putUInt32(info.mImagePixHeight);
+        mData.putUInt32(info.mImagePixDepth);
+        mData.putUInt32(info.mParent);
+        mData.putUInt16(info.mAssociationType);
+        mData.putUInt32(info.mAssociationDesc);
+        mData.putUInt32(info.mSequenceNumber);
+        mData.putString(info.mName);
+        mData.putEmptyString();    // date created
+        formatDateTime(info.mDateModified, date, sizeof(date));
+        mData.putString(date);   // date modified
+        mData.putEmptyString();   // keywords
+    }
+    return result;
 }
 
 MtpResponseCode MtpServer::doGetObject() {
@@ -641,12 +741,22 @@
     return MTP_RESPONSE_OK;
 }
 
-MtpResponseCode MtpServer::doGetPartialObject() {
+MtpResponseCode MtpServer::doGetPartialObject(MtpOperationCode operation) {
     if (!hasStorage())
         return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
     MtpObjectHandle handle = mRequest.getParameter(1);
-    uint32_t offset = mRequest.getParameter(2);
-    uint32_t length = mRequest.getParameter(3);
+    uint64_t offset;
+    uint32_t length;
+    offset = mRequest.getParameter(2);
+    if (operation == MTP_OPERATION_GET_PARTIAL_OBJECT_64) {
+        // android extension with 64 bit offset
+        uint64_t offset2 = mRequest.getParameter(3);
+        offset = offset | (offset2 << 32);
+        length = mRequest.getParameter(4);
+    } else {
+        // standard GetPartialObject
+        length = mRequest.getParameter(3);
+    }
     MtpString pathBuf;
     int64_t fileLength;
     MtpObjectFormat format;
@@ -933,4 +1043,113 @@
     return MTP_RESPONSE_OK;
 }
 
+MtpResponseCode MtpServer::doSendPartialObject() {
+    if (!hasStorage())
+        return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
+    MtpObjectHandle handle = mRequest.getParameter(1);
+    uint64_t offset = mRequest.getParameter(2);
+    uint64_t offset2 = mRequest.getParameter(3);
+    offset = offset | (offset2 << 32);
+    uint32_t length = mRequest.getParameter(4);
+
+    ObjectEdit* edit = getEditObject(handle);
+    if (!edit) {
+        LOGE("object not open for edit in doSendPartialObject");
+        return MTP_RESPONSE_GENERAL_ERROR;
+    }
+
+    // can't start writing past the end of the file
+    if (offset > edit->size) {
+        LOGD("writing past end of object, offset: %lld, edit->size: %lld", offset, edit->size);
+        return MTP_RESPONSE_GENERAL_ERROR;
+    }
+
+    // read the header
+    int ret = mData.readDataHeader(mFD);
+    // FIXME - check for errors here.
+
+    // reset so we don't attempt to send this back
+    mData.reset();
+
+    const char* filePath = (const char *)edit->path;
+    LOGV("receiving partial %s %lld %ld\n", filePath, offset, length);
+    mtp_file_range  mfr;
+    mfr.fd = edit->fd;
+    mfr.offset = offset;
+    mfr.length = length;
+
+    // transfer the file
+    ret = ioctl(mFD, MTP_RECEIVE_FILE, (unsigned long)&mfr);
+    LOGV("MTP_RECEIVE_FILE returned %d", ret);
+    if (ret < 0) {
+        mResponse.setParameter(1, 0);
+        if (errno == ECANCELED)
+            return MTP_RESPONSE_TRANSACTION_CANCELLED;
+        else
+            return MTP_RESPONSE_GENERAL_ERROR;
+    }
+    mResponse.setParameter(1, length);
+    uint64_t end = offset + length;
+    if (end > edit->size) {
+        edit->size = end;
+    }
+    return MTP_RESPONSE_OK;
+}
+
+MtpResponseCode MtpServer::doTruncateObject() {
+    MtpObjectHandle handle = mRequest.getParameter(1);
+    ObjectEdit* edit = getEditObject(handle);
+    if (!edit) {
+        LOGE("object not open for edit in doTruncateObject");
+        return MTP_RESPONSE_GENERAL_ERROR;
+    }
+
+    uint64_t offset = mRequest.getParameter(2);
+    uint64_t offset2 = mRequest.getParameter(3);
+    offset |= (offset2 << 32);
+    if (ftruncate(edit->fd, offset) != 0) {
+        return MTP_RESPONSE_GENERAL_ERROR;
+    } else {
+        edit->size = offset;
+        return MTP_RESPONSE_OK;
+    }
+}
+
+MtpResponseCode MtpServer::doBeginEditObject() {
+    MtpObjectHandle handle = mRequest.getParameter(1);
+    if (getEditObject(handle)) {
+        LOGE("object already open for edit in doBeginEditObject");
+        return MTP_RESPONSE_GENERAL_ERROR;
+    }
+
+    MtpString path;
+    int64_t fileLength;
+    MtpObjectFormat format;
+    int result = mDatabase->getObjectFilePath(handle, path, fileLength, format);
+    if (result != MTP_RESPONSE_OK)
+        return result;
+
+    int fd = open((const char *)path, O_RDWR | O_EXCL);
+    if (fd < 0) {
+        LOGE("open failed for %s in doBeginEditObject (%d)", (const char *)path, errno);
+        return MTP_RESPONSE_GENERAL_ERROR;
+    }
+
+    addEditObject(handle, path, fileLength, format, fd);
+    return MTP_RESPONSE_OK;
+}
+
+MtpResponseCode MtpServer::doEndEditObject() {
+    MtpObjectHandle handle = mRequest.getParameter(1);
+    ObjectEdit* edit = getEditObject(handle);
+    if (!edit) {
+        LOGE("object not open for edit in doEndEditObject");
+        return MTP_RESPONSE_GENERAL_ERROR;
+    }
+
+    commitEdit(edit);
+    removeEditObject(handle);
+    return MTP_RESPONSE_OK;
+}
+
 }  // namespace android