From c7eee6fef0263ed4eb5aa0d934a0a8c972d41b30 Mon Sep 17 00:00:00 2001 From: bigbiff bigbiff Date: Tue, 2 Sep 2014 18:59:01 -0400 Subject: add mtp responder to TWRP. Big thanks to Dees_Troy for helping with the implementation. Change-Id: I6c9c522b9c9de5dc139e2ecb0141008182ba07f0 --- mtp/MtpServer.cpp | 1337 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1337 insertions(+) create mode 100755 mtp/MtpServer.cpp (limited to 'mtp/MtpServer.cpp') diff --git a/mtp/MtpServer.cpp b/mtp/MtpServer.cpp new file mode 100755 index 000000000..5dec5801e --- /dev/null +++ b/mtp/MtpServer.cpp @@ -0,0 +1,1337 @@ +/* + * Copyright (C) 2010 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. + * + * Copyright (C) 2014 TeamWin - bigbiff and Dees_Troy mtp database conversion to C++ + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../twcommon.h" +#include + +#include "MtpTypes.h" +#include "MtpDebug.h" +#include "MtpDatabase.h" +#include "MtpObjectInfo.h" +#include "MtpProperty.h" +#include "MtpServer.h" +#include "MtpStorage.h" +#include "MtpStringBuffer.h" + +#include + +static const MtpOperationCode kSupportedOperationCodes[] = { + MTP_OPERATION_GET_DEVICE_INFO, + MTP_OPERATION_OPEN_SESSION, + MTP_OPERATION_CLOSE_SESSION, + MTP_OPERATION_GET_STORAGE_IDS, + MTP_OPERATION_GET_STORAGE_INFO, + MTP_OPERATION_GET_NUM_OBJECTS, + MTP_OPERATION_GET_OBJECT_HANDLES, + MTP_OPERATION_GET_OBJECT_INFO, + MTP_OPERATION_GET_OBJECT, + MTP_OPERATION_GET_THUMB, + MTP_OPERATION_DELETE_OBJECT, + MTP_OPERATION_SEND_OBJECT_INFO, + MTP_OPERATION_SEND_OBJECT, +// MTP_OPERATION_INITIATE_CAPTURE, +// MTP_OPERATION_FORMAT_STORE, +// MTP_OPERATION_RESET_DEVICE, +// MTP_OPERATION_SELF_TEST, +// MTP_OPERATION_SET_OBJECT_PROTECTION, +// MTP_OPERATION_POWER_DOWN, + MTP_OPERATION_GET_DEVICE_PROP_DESC, + MTP_OPERATION_GET_DEVICE_PROP_VALUE, + MTP_OPERATION_SET_DEVICE_PROP_VALUE, + MTP_OPERATION_RESET_DEVICE_PROP_VALUE, +// MTP_OPERATION_TERMINATE_OPEN_CAPTURE, +// MTP_OPERATION_MOVE_OBJECT, +// MTP_OPERATION_COPY_OBJECT, + MTP_OPERATION_GET_PARTIAL_OBJECT, +// MTP_OPERATION_INITIATE_OPEN_CAPTURE, + MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED, + MTP_OPERATION_GET_OBJECT_PROP_DESC, + MTP_OPERATION_GET_OBJECT_PROP_VALUE, + MTP_OPERATION_SET_OBJECT_PROP_VALUE, + MTP_OPERATION_GET_OBJECT_PROP_LIST, +// MTP_OPERATION_SET_OBJECT_PROP_LIST, +// MTP_OPERATION_GET_INTERDEPENDENT_PROP_DESC, +// MTP_OPERATION_SEND_OBJECT_PROP_LIST, + 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[] = { + MTP_EVENT_OBJECT_ADDED, + MTP_EVENT_OBJECT_REMOVED, + MTP_EVENT_STORE_ADDED, + MTP_EVENT_STORE_REMOVED, + MTP_EVENT_OBJECT_PROP_CHANGED, +}; + +MtpServer::MtpServer(int fd, MtpDatabase* database, bool ptp, + int fileGroup, int filePerm, int directoryPerm) + : mFD(fd), + mDatabase(database), + mPtp(ptp), + mFileGroup(fileGroup), + mFilePermission(filePerm), + mDirectoryPermission(directoryPerm), + mSessionID(0), + mSessionOpen(false), + mSendObjectHandle(kInvalidObjectHandle), + mSendObjectFormat(0), + mSendObjectFileSize(0) +{ +} + +MtpServer::~MtpServer() { +} + +void MtpServer::addStorage(MtpStorage* storage) { + MTPD("addStorage(): storage: %x\n", storage); + mDatabase->createDB(storage, storage->getStorageID()); + android::Mutex::Autolock autoLock(mMutex); + mStorages.push(storage); + sendStoreAdded(storage->getStorageID()); +} + +void MtpServer::removeStorage(MtpStorage* storage) { + android::Mutex::Autolock autoLock(mMutex); + + for (size_t i = 0; i < mStorages.size(); i++) { + if (mStorages[i] == storage) { + mStorages.removeAt(i); + sendStoreRemoved(storage->getStorageID()); + break; + } + } +} + +MtpStorage* MtpServer::getStorage(MtpStorageID id) { + MTPD("getStorage\n"); + if (id == 0) { + MTPD("mStorages\n"); + return mStorages[0]; + } + for (size_t i = 0; i < mStorages.size(); i++) { + MtpStorage* storage = mStorages[i]; + MTPD("id: %d\n", id); + MTPD("storage: %d\n", storage->getStorageID()); + if (storage->getStorageID() == id) { + return storage; + } + } + return NULL; +} + +bool MtpServer::hasStorage(MtpStorageID id) { + MTPD("in hasStorage\n"); + if (id == 0 || id == 0xFFFFFFFF) + return mStorages.size() > 0; + return (getStorage(id) != NULL); +} + +void MtpServer::run() { + int fd = mFD; + + MTPI("MtpServer::run fd: %d\n", fd); + + while (1) { + MTPD("About to read device...\n"); + int ret = mRequest.read(fd); + if (ret < 0) { + MTPD("request read returned %d, errno: %d", ret, errno); + if (errno == ECANCELED) { + // return to top of loop and wait for next command + continue; + } + break; + } + MtpOperationCode operation = mRequest.getOperationCode(); + MtpTransactionID transaction = mRequest.getTransactionID(); + + MTPD("operation: %s", MtpDebug::getOperationCodeName(operation)); + mRequest.dump(); + + // FIXME need to generalize this + bool dataIn = (operation == MTP_OPERATION_SEND_OBJECT_INFO + || operation == MTP_OPERATION_SET_OBJECT_REFERENCES + || operation == MTP_OPERATION_SET_OBJECT_PROP_VALUE + || operation == MTP_OPERATION_SET_DEVICE_PROP_VALUE); + if (dataIn) { + int ret = mData.read(fd); + if (ret < 0) { + MTPD("data read returned %d, errno: %d", ret, errno); + if (errno == ECANCELED) { + // return to top of loop and wait for next command + continue; + } + break; + } + MTPD("received data:"); + mData.dump(); + } else { + mData.reset(); + } + + if (handleRequest()) { + if (!dataIn && mData.hasData()) { + mData.setOperationCode(operation); + mData.setTransactionID(transaction); + MTPD("sending data:"); + mData.dump(); + ret = mData.write(fd); + if (ret < 0) { + MTPD("request write returned %d, errno: %d", ret, errno); + if (errno == ECANCELED) { + // return to top of loop and wait for next command + continue; + } + break; + } + } + + mResponse.setTransactionID(transaction); + MTPD("sending response %04X\n", mResponse.getResponseCode()); + ret = mResponse.write(fd); + MTPD("ret: %d\n", ret); + mResponse.dump(); + if (ret < 0) { + MTPD("request write returned %d, errno: %d", ret, errno); + if (errno == ECANCELED) { + // return to top of loop and wait for next command + continue; + } + break; + } + } else { + MTPD("skipping response\n"); + } + } + + // 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(); + close(fd); + mFD = -1; +} + +void MtpServer::sendObjectAdded(MtpObjectHandle handle) { + MTPD("sendObjectAdded %d\n", handle); + sendEvent(MTP_EVENT_OBJECT_ADDED, handle); +} + +void MtpServer::sendObjectRemoved(MtpObjectHandle handle) { + MTPD("sendObjectRemoved %d\n", handle); + sendEvent(MTP_EVENT_OBJECT_REMOVED, handle); +} + +void MtpServer::sendObjectUpdated(MtpObjectHandle handle) { + MTPD("sendObjectUpdated %d\n", handle); + sendEvent(MTP_EVENT_OBJECT_PROP_CHANGED, handle); +} + +void MtpServer::sendStoreAdded(MtpStorageID id) { + MTPD("sendStoreAdded %08X\n", id); + sendEvent(MTP_EVENT_STORE_ADDED, id); +} + +void MtpServer::sendStoreRemoved(MtpStorageID id) { + MTPD("sendStoreRemoved %08X\n", id); + sendEvent(MTP_EVENT_STORE_REMOVED, id); +} + +void MtpServer::sendEvent(MtpEventCode code, uint32_t param1) { + MTPD("MtpServer::sendEvent sending event code: %x\n", code); + if (mSessionOpen) { + mEvent.setEventCode(code); + mEvent.setTransactionID(mRequest.getTransactionID()); + mEvent.setParameter(1, param1); + int ret = mEvent.write(mFD); + MTPD("mEvent.write returned %d\n", ret); + } +} + +void MtpServer::addEditObject(MtpObjectHandle handle, MtpString& path, + uint64_t size, MtpObjectFormat format, int fd) { + ObjectEdit* edit = new ObjectEdit(handle, path, size, format, 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->mHandle == 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->mHandle == handle) { + delete edit; + mObjectEditList.removeAt(i); + return; + } + } + MTPE("ObjectEdit not found in removeEditObject"); +} + +void MtpServer::commitEdit(ObjectEdit* edit) { + mDatabase->endSendObject((const char *)edit->mPath, edit->mHandle, edit->mFormat, true); +} + + +bool MtpServer::handleRequest() { + android::Mutex::Autolock autoLock(mMutex); + + MtpOperationCode operation = mRequest.getOperationCode(); + MtpResponseCode response; + + mResponse.reset(); + + if (mSendObjectHandle != kInvalidObjectHandle && operation != MTP_OPERATION_SEND_OBJECT) { + // FIXME - need to delete mSendObjectHandle from the database + MTPE("expected SendObject after SendObjectInfo"); + mSendObjectHandle = kInvalidObjectHandle; + } + + switch (operation) { + case MTP_OPERATION_GET_DEVICE_INFO: + MTPD("doGetDeviceInfo()\n"); + response = doGetDeviceInfo(); + break; + case MTP_OPERATION_OPEN_SESSION: + MTPD("doOpenSesion()\n"); + response = doOpenSession(); + break; + case MTP_OPERATION_CLOSE_SESSION: + MTPD("doCloseSession()\n"); + response = doCloseSession(); + break; + case MTP_OPERATION_GET_STORAGE_IDS: + MTPD("doGetStorageIDs()\n"); + response = doGetStorageIDs(); + break; + case MTP_OPERATION_GET_STORAGE_INFO: + MTPD("about to call doGetStorageInfo()\n"); + response = doGetStorageInfo(); + break; + case MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED: + MTPD("about to call doGetObjectPropsSupported()\n"); + response = doGetObjectPropsSupported(); + break; + case MTP_OPERATION_GET_OBJECT_HANDLES: + MTPD("about to call doGetObjectHandles()\n"); + response = doGetObjectHandles(); + break; + case MTP_OPERATION_GET_NUM_OBJECTS: + MTPD("about to call doGetNumbObjects()\n"); + response = doGetNumObjects(); + break; + case MTP_OPERATION_GET_OBJECT_REFERENCES: + MTPD("about to call doGetObjectReferences()\n"); + response = doGetObjectReferences(); + break; + case MTP_OPERATION_SET_OBJECT_REFERENCES: + MTPD("about to call doSetObjectReferences()\n"); + response = doSetObjectReferences(); + break; + case MTP_OPERATION_GET_OBJECT_PROP_VALUE: + MTPD("about to call doGetObjectPropValue()\n"); + response = doGetObjectPropValue(); + break; + case MTP_OPERATION_SET_OBJECT_PROP_VALUE: + MTPD("about to call doSetObjectPropValue()\n"); + response = doSetObjectPropValue(); + break; + case MTP_OPERATION_GET_DEVICE_PROP_VALUE: + MTPD("about to call doGetDevicPropValue()\n"); + response = doGetDevicePropValue(); + break; + case MTP_OPERATION_SET_DEVICE_PROP_VALUE: + MTPD("about to call doSetDevicePropVaue()\n"); + response = doSetDevicePropValue(); + break; + case MTP_OPERATION_RESET_DEVICE_PROP_VALUE: + MTPD("about to call doResetDevicePropValue()\n"); + response = doResetDevicePropValue(); + break; + case MTP_OPERATION_GET_OBJECT_PROP_LIST: + MTPD("calling doGetObjectPropList()\n"); + response = doGetObjectPropList(); + break; + case MTP_OPERATION_GET_OBJECT_INFO: + MTPD("calling doGetObjectInfo()\n"); + response = doGetObjectInfo(); + break; + case MTP_OPERATION_GET_OBJECT: + MTPD("about to call doGetObject()\n"); + response = doGetObject(); + break; + case MTP_OPERATION_GET_THUMB: + response = doGetThumb(); + break; + case MTP_OPERATION_GET_PARTIAL_OBJECT: + case MTP_OPERATION_GET_PARTIAL_OBJECT_64: + response = doGetPartialObject(operation); + break; + case MTP_OPERATION_SEND_OBJECT_INFO: + MTPD("about to call doSendObjectInfo()\n"); + response = doSendObjectInfo(); + break; + case MTP_OPERATION_SEND_OBJECT: + MTPD("about to call doSendObject()\n"); + response = doSendObject(); + break; + case MTP_OPERATION_DELETE_OBJECT: + response = doDeleteObject(); + break; + case MTP_OPERATION_GET_OBJECT_PROP_DESC: + MTPD("about to call doGetObjectPropDesc()\n"); + response = doGetObjectPropDesc(); + break; + case MTP_OPERATION_GET_DEVICE_PROP_DESC: + MTPD("about to call doGetDevicePropDesc()\n"); + 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: + MTPE("got unsupported command %s", MtpDebug::getOperationCodeName(operation)); + response = MTP_RESPONSE_OPERATION_NOT_SUPPORTED; + break; + } + + if (response == MTP_RESPONSE_TRANSACTION_CANCELLED) + return false; + mResponse.setResponseCode(response); + return true; +} + +MtpResponseCode MtpServer::doGetDeviceInfo() { + MtpStringBuffer string; + char prop_value[PROPERTY_VALUE_MAX]; + + MtpObjectFormatList* playbackFormats = mDatabase->getSupportedPlaybackFormats(); + MtpObjectFormatList* captureFormats = mDatabase->getSupportedCaptureFormats(); + MtpDevicePropertyList* deviceProperties = mDatabase->getSupportedDeviceProperties(); + + // fill in device info + mData.putUInt16(MTP_STANDARD_VERSION); + if (mPtp) { + MTPD("doGetDeviceInfo putting 0\n"); + mData.putUInt32(0); + } else { + // MTP Vendor Extension ID + MTPD("doGetDeviceInfo putting 6\n"); + mData.putUInt32(6); + } + mData.putUInt16(MTP_STANDARD_VERSION); + if (mPtp) { + // no extensions + MTPD("doGetDeviceInfo no extensions\n"); + string.set(""); + } else { + // MTP extensions + MTPD("doGetDeviceInfo microsoft.com: 1.0; android.com: 1.0;\n"); + string.set("microsoft.com: 1.0; android.com: 1.0;"); + } + mData.putString(string); // MTP Extensions + mData.putUInt16(0); //Functional Mode + MTPD("doGetDeviceInfo opcodes, %i\n", sizeof(kSupportedOperationCodes) / sizeof(uint16_t)); + MTPD("doGetDeviceInfo eventcodes, %i\n", sizeof(kSupportedEventCodes) / sizeof(uint16_t)); + mData.putAUInt16(kSupportedOperationCodes, + sizeof(kSupportedOperationCodes) / sizeof(uint16_t)); // Operations Supported + mData.putAUInt16(kSupportedEventCodes, + sizeof(kSupportedEventCodes) / sizeof(uint16_t)); // Events Supported + mData.putAUInt16(deviceProperties); // Device Properties Supported + mData.putAUInt16(captureFormats); // Capture Formats + mData.putAUInt16(playbackFormats); // Playback Formats + + property_get("ro.product.manufacturer", prop_value, "unknown manufacturer"); + MTPD("prop: %s\n", prop_value); + string.set(prop_value); + mData.putString(string); // Manufacturer + + property_get("ro.product.model", prop_value, "MTP Device"); + string.set(prop_value); + mData.putString(string); // Model + string.set("1.0"); + mData.putString(string); // Device Version + + property_get("ro.serialno", prop_value, "????????"); + MTPD("sn: %s\n", prop_value); + string.set(prop_value); + mData.putString(string); // Serial Number + + delete playbackFormats; + delete captureFormats; + delete deviceProperties; + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doOpenSession() { + if (mSessionOpen) { + mResponse.setParameter(1, mSessionID); + return MTP_RESPONSE_SESSION_ALREADY_OPEN; + } + mSessionID = mRequest.getParameter(1); + mSessionOpen = true; + + mDatabase->sessionStarted(); + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doCloseSession() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + mSessionID = 0; + mSessionOpen = false; + mDatabase->sessionEnded(); + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetStorageIDs() { + MTPD("doGetStorageIDs()\n"); + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + int count = mStorages.size(); + mData.putUInt32(count); + for (int i = 0; i < count; i++) { + MTPD("getting storageid %d\n", mStorages[i]->getStorageID()); + mData.putUInt32(mStorages[i]->getStorageID()); + } + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetStorageInfo() { + MtpStringBuffer string; + MTPD("doGetStorageInfo()\n"); + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpStorageID id = mRequest.getParameter(1); + MtpStorage* storage = getStorage(id); + if (!storage) { + MTPE("invalid storage id\n"); + return MTP_RESPONSE_INVALID_STORAGE_ID; + } + + mData.putUInt16(storage->getType()); + mData.putUInt16(storage->getFileSystemType()); + mData.putUInt16(storage->getAccessCapability()); + mData.putUInt64(storage->getMaxCapacity()); + mData.putUInt64(storage->getFreeSpace()); + mData.putUInt32(1024*1024*1024); // Free Space in Objects + string.set(storage->getDescription()); + mData.putString(string); + mData.putEmptyString(); // Volume Identifier + + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetObjectPropsSupported() { + MTPD("doGetObjectPropsSupported()\n"); + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpObjectFormat format = mRequest.getParameter(1); + mDatabase->lockMutex(); + MtpObjectPropertyList* properties = mDatabase->getSupportedObjectProperties(format); + mData.putAUInt16(properties); + delete properties; + mDatabase->unlockMutex(); + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetObjectHandles() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpStorageID storageID = mRequest.getParameter(1); // 0xFFFFFFFF for all storage + MtpObjectFormat format = mRequest.getParameter(2); // 0 for all formats + MtpObjectHandle parent = mRequest.getParameter(3); // 0xFFFFFFFF for objects with no parent + // 0x00000000 for all objects + + if (!hasStorage(storageID)) + return MTP_RESPONSE_INVALID_STORAGE_ID; + + MTPD("calling MtpDatabase->getObjectList()\n"); + mDatabase->lockMutex(); + MtpObjectHandleList* handles = mDatabase->getObjectList(storageID, format, parent); + mData.putAUInt32(handles); + delete handles; + mDatabase->unlockMutex(); + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetNumObjects() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + MtpStorageID storageID = mRequest.getParameter(1); // 0xFFFFFFFF for all storage + MtpObjectFormat format = mRequest.getParameter(2); // 0 for all formats + MtpObjectHandle parent = mRequest.getParameter(3); // 0xFFFFFFFF for objects with no parent + // 0x00000000 for all objects + if (!hasStorage(storageID)) + return MTP_RESPONSE_INVALID_STORAGE_ID; + + mDatabase->lockMutex(); + int count = mDatabase->getNumObjects(storageID, format, parent); + mDatabase->unlockMutex(); + if (count >= 0) { + mResponse.setParameter(1, count); + return MTP_RESPONSE_OK; + } else { + mResponse.setParameter(1, 0); + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + } +} + +MtpResponseCode MtpServer::doGetObjectReferences() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + + // FIXME - check for invalid object handle + mDatabase->lockMutex(); + MtpObjectHandleList* handles = mDatabase->getObjectReferences(handle); + if (handles) { + mData.putAUInt32(handles); + delete handles; + } else { + MTPD("MtpServer::doGetObjectReferences putEmptyArray\n"); + mData.putEmptyArray(); + } + mDatabase->unlockMutex(); + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doSetObjectReferences() { + if (!mSessionOpen) + return MTP_RESPONSE_SESSION_NOT_OPEN; + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpStorageID handle = mRequest.getParameter(1); + + MtpObjectHandleList* references = mData.getAUInt32(); + mDatabase->lockMutex(); + MtpResponseCode result = mDatabase->setObjectReferences(handle, references); + mDatabase->unlockMutex(); + delete references; + return result; +} + +MtpResponseCode MtpServer::doGetObjectPropValue() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectProperty property = mRequest.getParameter(2); + MTPD("GetObjectPropValue %d %s\n", handle, + MtpDebug::getObjectPropCodeName(property)); + + mDatabase->lockMutex(); + MtpResponseCode res = mDatabase->getObjectPropertyValue(handle, property, mData); + mDatabase->unlockMutex(); + return res; +} + +MtpResponseCode MtpServer::doSetObjectPropValue() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectProperty property = mRequest.getParameter(2); + MTPD("SetObjectPropValue %d %s\n", handle, + MtpDebug::getObjectPropCodeName(property)); + + mDatabase->lockMutex(); + MtpResponseCode res = mDatabase->setObjectPropertyValue(handle, property, mData); + mDatabase->unlockMutex(); + return res; +} + +MtpResponseCode MtpServer::doGetDevicePropValue() { + MtpDeviceProperty property = mRequest.getParameter(1); + MTPD("GetDevicePropValue %s\n", + MtpDebug::getDevicePropCodeName(property)); + + mDatabase->lockMutex(); + MtpResponseCode res = mDatabase->getDevicePropertyValue(property, mData); + mDatabase->unlockMutex(); + return res; +} + +MtpResponseCode MtpServer::doSetDevicePropValue() { + MtpDeviceProperty property = mRequest.getParameter(1); + MTPD("SetDevicePropValue %s\n", + MtpDebug::getDevicePropCodeName(property)); + + mDatabase->lockMutex(); + MtpResponseCode res = mDatabase->setDevicePropertyValue(property, mData); + mDatabase->unlockMutex(); + return res; +} + +MtpResponseCode MtpServer::doResetDevicePropValue() { + MtpDeviceProperty property = mRequest.getParameter(1); + MTPD("ResetDevicePropValue %s\n", + MtpDebug::getDevicePropCodeName(property)); + + mDatabase->lockMutex(); + MtpResponseCode res = mDatabase->resetDeviceProperty(property); + mDatabase->unlockMutex(); + return res; +} + +MtpResponseCode MtpServer::doGetObjectPropList() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + + MtpObjectHandle handle = mRequest.getParameter(1); + // use uint32_t so we can support 0xFFFFFFFF + uint32_t format = mRequest.getParameter(2); + uint32_t property = mRequest.getParameter(3); + int groupCode = mRequest.getParameter(4); + int depth = mRequest.getParameter(5); + MTPD("GetObjectPropList %d format: %s property: %x group: %d depth: %d\n", + handle, MtpDebug::getFormatCodeName(format), + property, groupCode, depth); + + mDatabase->lockMutex(); + MtpResponseCode res = mDatabase->getObjectPropertyList(handle, format, property, groupCode, depth, mData); + mDatabase->unlockMutex(); + return res; +} + +MtpResponseCode MtpServer::doGetObjectInfo() { + MTPD("inside doGetObjectInfo()\n"); + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectInfo info(handle); + MTPD("calling mtpdatabase getObjectInfo()\n"); + mDatabase->lockMutex(); + MtpResponseCode result = mDatabase->getObjectInfo(handle, info); + mDatabase->unlockMutex(); + 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->mSize > 0xFFFFFFFFLL ? 0xFFFFFFFF : (uint32_t)edit->mSize); + 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); + MTPD("info.mName: %s\n", info.mName); + 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() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpString pathBuf; + int64_t fileLength; + MtpObjectFormat format; + MTPD("MtpServer::doGetObject calling getObjectFilePath\n"); + mDatabase->lockMutex(); + int result = mDatabase->getObjectFilePath(handle, pathBuf, fileLength, format); + mDatabase->unlockMutex(); + if (result != MTP_RESPONSE_OK) + return result; + + const char* filePath = (const char *)pathBuf; + MTPD("filePath: %s\n", filePath); + mtp_file_range mfr; + mfr.fd = open(filePath, O_RDONLY); + if (mfr.fd < 0) { + return MTP_RESPONSE_GENERAL_ERROR; + } + mfr.offset = 0; + mfr.length = fileLength; + MTPD("mfr.length: %lld\n", mfr.length); + mfr.command = mRequest.getOperationCode(); + mfr.transaction_id = mRequest.getTransactionID(); + + // then transfer the file + int ret = ioctl(mFD, MTP_SEND_FILE_WITH_HEADER, (unsigned long)&mfr); + MTPD("MTP_SEND_FILE_WITH_HEADER returned %d\n", ret); + close(mfr.fd); + if (ret < 0) { + if (errno == ECANCELED) + return MTP_RESPONSE_TRANSACTION_CANCELLED; + else + return MTP_RESPONSE_GENERAL_ERROR; + } + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetThumb() { + MtpObjectHandle handle = mRequest.getParameter(1); + size_t thumbSize; + mDatabase->lockMutex(); + void* thumb = mDatabase->getThumbnail(handle, thumbSize); + mDatabase->unlockMutex(); + if (thumb) { + // send data + mData.setOperationCode(mRequest.getOperationCode()); + mData.setTransactionID(mRequest.getTransactionID()); + mData.writeData(mFD, thumb, thumbSize); + free(thumb); + return MTP_RESPONSE_OK; + } else { + return MTP_RESPONSE_GENERAL_ERROR; + } +} + +MtpResponseCode MtpServer::doGetPartialObject(MtpOperationCode operation) { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + 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; + MTPD("MtpServer::doGetPartialObject calling getObjectFilePath\n"); + mDatabase->lockMutex(); + int result = mDatabase->getObjectFilePath(handle, pathBuf, fileLength, format); + mDatabase->unlockMutex(); + if (result != MTP_RESPONSE_OK) { + return result; + } + if (offset + length > (uint64_t)fileLength) + length = fileLength - offset; + + const char* filePath = (const char *)pathBuf; + mtp_file_range mfr; + mfr.fd = open(filePath, O_RDONLY); + if (mfr.fd < 0) { + return MTP_RESPONSE_GENERAL_ERROR; + } + mfr.offset = offset; + mfr.length = length; + mfr.command = mRequest.getOperationCode(); + mfr.transaction_id = mRequest.getTransactionID(); + mResponse.setParameter(1, length); + + // transfer the file + int ret = ioctl(mFD, MTP_SEND_FILE_WITH_HEADER, (unsigned long)&mfr); + MTPD("MTP_SEND_FILE_WITH_HEADER returned %d\n", ret); + close(mfr.fd); + if (ret < 0) { + if (errno == ECANCELED) + return MTP_RESPONSE_TRANSACTION_CANCELLED; + else + return MTP_RESPONSE_GENERAL_ERROR; + } + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doSendObjectInfo() { + MTPD("MtpServer::doSendObjectInfo starting\n"); + MtpString path; + MtpStorageID storageID = mRequest.getParameter(1); + MtpStorage* storage = getStorage(storageID); + MtpObjectHandle parent = mRequest.getParameter(2); + if (!storage) + return MTP_RESPONSE_INVALID_STORAGE_ID; + + // special case the root + if (parent == MTP_PARENT_ROOT) { + MTPD("MtpServer::doSendObjectInfo special case root\n"); + path = storage->getPath(); + parent = 0; + } else { + int64_t length; + MtpObjectFormat format; + MTPD("MtpServer::doSendObjectInfo calling getObjectFilePath\n"); + mDatabase->lockMutex(); + int result = mDatabase->getObjectFilePath(parent, path, length, format); + mDatabase->unlockMutex(); + if (result != MTP_RESPONSE_OK) { + return result; + } + if (format != MTP_FORMAT_ASSOCIATION) + return MTP_RESPONSE_INVALID_PARENT_OBJECT; + } + + // read only the fields we need + mData.getUInt32(); // storage ID + MtpObjectFormat format = mData.getUInt16(); + mData.getUInt16(); // protection status + mSendObjectFileSize = mData.getUInt32(); + mData.getUInt16(); // thumb format + mData.getUInt32(); // thumb compressed size + mData.getUInt32(); // thumb pix width + mData.getUInt32(); // thumb pix height + mData.getUInt32(); // image pix width + mData.getUInt32(); // image pix height + mData.getUInt32(); // image bit depth + mData.getUInt32(); // parent + uint16_t associationType = mData.getUInt16(); + uint32_t associationDesc = mData.getUInt32(); // association desc + mData.getUInt32(); // sequence number + MtpStringBuffer name, created, modified; + mData.getString(name); // file name + mData.getString(created); // date created + mData.getString(modified); // date modified + // keywords follow + + MTPD("name: %s format: %04X\n", (const char *)name, format); + time_t modifiedTime; + if (!parseDateTime(modified, modifiedTime)) { + modifiedTime = 0; + } + if (path[path.size() - 1] != '/') { + path += "/"; + } + path += (const char *)name; + + // check space first + if (mSendObjectFileSize > storage->getFreeSpace()) + return MTP_RESPONSE_STORAGE_FULL; + uint64_t maxFileSize = storage->getMaxFileSize(); + // check storage max file size + if (maxFileSize != 0) { + // if mSendObjectFileSize is 0xFFFFFFFF, then all we know is the file size + // is >= 0xFFFFFFFF + if (mSendObjectFileSize > maxFileSize || mSendObjectFileSize == 0xFFFFFFFF) + return MTP_RESPONSE_OBJECT_TOO_LARGE; + } + + MTPD("MtpServer::doSendObjectInfo path: %s parent: %d storageID: %08X\n", (const char*)path, parent, storageID); + mDatabase->lockMutex(); + MtpObjectHandle handle = mDatabase->beginSendObject((const char*)path, + format, parent, storageID, mSendObjectFileSize, modifiedTime); + mDatabase->unlockMutex(); + if (handle == kInvalidObjectHandle) { + MTPE("MtpServer::doSendObjectInfo returning MTP_RESPONSE_GENERAL_ERROR, handle == kInvalidObjectHandle\n"); + return MTP_RESPONSE_GENERAL_ERROR; + } + + if (format == MTP_FORMAT_ASSOCIATION) { + mode_t mask = umask(0); + MTPD("MtpServer::doSendObjectInfo mkdir '%s'\n", (const char *)path); + int ret = mkdir((const char *)path, mDirectoryPermission); + umask(mask); + if (ret && ret != -EEXIST) { + MTPE("MtpServer::doSendObjectInfo returning MTP_RESPONSE_GENERAL_ERROR, ret && ret != -EEXIST\n"); + return MTP_RESPONSE_GENERAL_ERROR; + } + chown((const char *)path, getuid(), mFileGroup); + + // SendObject does not get sent for directories, so call endSendObject here instead + mDatabase->lockMutex(); + mDatabase->endSendObject(path, handle, MTP_FORMAT_ASSOCIATION, MTP_RESPONSE_OK); + mDatabase->unlockMutex(); + } else { + mSendObjectFilePath = path; + // save the handle for the SendObject call, which should follow + mSendObjectHandle = handle; + mSendObjectFormat = format; + } + + mResponse.setParameter(1, storageID); + mResponse.setParameter(2, parent); + mResponse.setParameter(3, handle); + MTPD("MtpServer::doSendObjectInfo returning MTP_RESPONSE_OK\n"); + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doSendObject() { + if (!hasStorage()) + return MTP_RESPONSE_GENERAL_ERROR; + MtpResponseCode result = MTP_RESPONSE_OK; + mode_t mask; + int ret = 0, initialData; + + if (mSendObjectHandle == kInvalidObjectHandle) { + MTPE("Expected SendObjectInfo before SendObject"); + result = MTP_RESPONSE_NO_VALID_OBJECT_INFO; + goto done; + } + + // read the header, and possibly some data + ret = mData.read(mFD); + if (ret < MTP_CONTAINER_HEADER_SIZE) { + MTPE("MTP_RESPONSE_GENERAL_ERROR\n"); + result = MTP_RESPONSE_GENERAL_ERROR; + goto done; + } + initialData = ret - MTP_CONTAINER_HEADER_SIZE; + + mtp_file_range mfr; + mfr.fd = open(mSendObjectFilePath, O_RDWR | O_CREAT | O_TRUNC, 0640); + if (mfr.fd < 0) { + result = MTP_RESPONSE_GENERAL_ERROR; + MTPE("fd error\n"); + goto done; + } + fchown(mfr.fd, getuid(), mFileGroup); + // set permissions + mask = umask(0); + fchmod(mfr.fd, mFilePermission); + umask(mask); + + if (initialData > 0) + ret = write(mfr.fd, mData.getData(), initialData); + + if (mSendObjectFileSize - initialData > 0) { + mfr.offset = initialData; + if (mSendObjectFileSize == 0xFFFFFFFF) { + // tell driver to read until it receives a short packet + mfr.length = 0xFFFFFFFF; + } else { + mfr.length = mSendObjectFileSize - initialData; + } + + MTPD("receiving %s\n", (const char *)mSendObjectFilePath); + // transfer the file + ret = ioctl(mFD, MTP_RECEIVE_FILE, (unsigned long)&mfr); + } + close(mfr.fd); + + if (ret < 0) { + unlink(mSendObjectFilePath); + if (errno == ECANCELED) + result = MTP_RESPONSE_TRANSACTION_CANCELLED; + else + result = MTP_RESPONSE_GENERAL_ERROR; + } + +done: + // reset so we don't attempt to send the data back + MTPD("MTP_RECEIVE_FILE returned %d\n", ret); + mData.reset(); + mDatabase->lockMutex(); + mDatabase->endSendObject(mSendObjectFilePath, mSendObjectHandle, mSendObjectFormat, + result == MTP_RESPONSE_OK); + mDatabase->unlockMutex(); + mSendObjectHandle = kInvalidObjectHandle; + MTPD("result: %d\n", result); + mSendObjectFormat = 0; + return MTP_RESPONSE_OK; +} + +static void deleteRecursive(const char* path) { + char pathbuf[PATH_MAX]; + size_t pathLength = strlen(path); + if (pathLength >= sizeof(pathbuf) - 1) { + MTPE("path too long: %s\n", path); + } + strcpy(pathbuf, path); + if (pathbuf[pathLength - 1] != '/') { + pathbuf[pathLength++] = '/'; + } + char* fileSpot = pathbuf + pathLength; + int pathRemaining = sizeof(pathbuf) - pathLength - 1; + + DIR* dir = opendir(path); + if (!dir) { + MTPE("opendir %s failed: %s", path, strerror(errno)); + return; + } + + struct dirent* entry; + while ((entry = readdir(dir))) { + const char* name = entry->d_name; + + // ignore "." and ".." + if (name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0))) { + continue; + } + + int nameLength = strlen(name); + if (nameLength > pathRemaining) { + MTPE("path %s/%s too long\n", path, name); + continue; + } + strcpy(fileSpot, name); + + int type = entry->d_type; + if (entry->d_type == DT_DIR) { + deleteRecursive(pathbuf); + rmdir(pathbuf); + } else { + unlink(pathbuf); + } + } + closedir(dir); +} + +static void deletePath(const char* path) { + struct stat statbuf; + if (stat(path, &statbuf) == 0) { + if (S_ISDIR(statbuf.st_mode)) { + deleteRecursive(path); + rmdir(path); + } else { + unlink(path); + } + } else { + MTPE("deletePath stat failed for %s: %s", path, strerror(errno)); + } +} + +MtpResponseCode MtpServer::doDeleteObject() { + if (!hasStorage()) + return MTP_RESPONSE_INVALID_OBJECT_HANDLE; + MtpObjectHandle handle = mRequest.getParameter(1); + MtpObjectFormat format = mRequest.getParameter(2); + // FIXME - support deleting all objects if handle is 0xFFFFFFFF + // FIXME - implement deleting objects by format + + MtpString filePath; + int64_t fileLength; + MTPD("MtpServer::doDeleteObject calling getObjectFilePath\n"); + mDatabase->lockMutex(); + int result = mDatabase->getObjectFilePath(handle, filePath, fileLength, format); + if (result == MTP_RESPONSE_OK) { + MTPD("deleting %s", (const char *)filePath); + result = mDatabase->deleteFile(handle); + // Don't delete the actual files unless the database deletion is allowed + if (result == MTP_RESPONSE_OK) { + deletePath((const char *)filePath); + } + } + mDatabase->unlockMutex(); + return result; +} + +MtpResponseCode MtpServer::doGetObjectPropDesc() { + MtpObjectProperty propCode = mRequest.getParameter(1); + MtpObjectFormat format = mRequest.getParameter(2); + MTPD("MtpServer::doGetObjectPropDesc %s %s\n", MtpDebug::getObjectPropCodeName(propCode), + MtpDebug::getFormatCodeName(format)); + mDatabase->lockMutex(); + MtpProperty* property = mDatabase->getObjectPropertyDesc(propCode, format); + mDatabase->unlockMutex(); + if (!property) { + MTPE("MtpServer::doGetObjectPropDesc propery not supported\n"); + return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED; + } + property->write(mData); + delete property; + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doGetDevicePropDesc() { + MtpDeviceProperty propCode = mRequest.getParameter(1); + MTPD("GetDevicePropDesc %s\n", MtpDebug::getDevicePropCodeName(propCode)); + mDatabase->lockMutex(); + MtpProperty* property = mDatabase->getDevicePropertyDesc(propCode); + mDatabase->unlockMutex(); + if (!property) { + MTPE("MtpServer::doGetDevicePropDesc property not supported\n"); + return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED; + } + property->write(mData); + delete property; + 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) { + MTPE("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->mSize) { + MTPE("writing past end of object, offset: %lld, edit->mSize: %lld", offset, edit->mSize); + return MTP_RESPONSE_GENERAL_ERROR; + } + + const char* filePath = (const char *)edit->mPath; + MTPD("receiving partial %s %lld %lld\n", filePath, offset, length); + + // read the header, and possibly some data + int ret = mData.read(mFD); + if (ret < MTP_CONTAINER_HEADER_SIZE) + return MTP_RESPONSE_GENERAL_ERROR; + int initialData = ret - MTP_CONTAINER_HEADER_SIZE; + + if (initialData > 0) { + ret = write(edit->mFD, mData.getData(), initialData); + offset += initialData; + length -= initialData; + } + + if (length > 0) { + mtp_file_range mfr; + mfr.fd = edit->mFD; + mfr.offset = offset; + mfr.length = length; + + // transfer the file + ret = ioctl(mFD, MTP_RECEIVE_FILE, (unsigned long)&mfr); + MTPD("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; + } + + // reset so we don't attempt to send this back + mData.reset(); + mResponse.setParameter(1, length); + uint64_t end = offset + length; + if (end > edit->mSize) { + edit->mSize = end; + } + return MTP_RESPONSE_OK; +} + +MtpResponseCode MtpServer::doTruncateObject() { + MtpObjectHandle handle = mRequest.getParameter(1); + ObjectEdit* edit = getEditObject(handle); + if (!edit) { + MTPE("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->mFD, offset) != 0) { + return MTP_RESPONSE_GENERAL_ERROR; + } else { + edit->mSize = offset; + return MTP_RESPONSE_OK; + } +} + +MtpResponseCode MtpServer::doBeginEditObject() { + MtpObjectHandle handle = mRequest.getParameter(1); + if (getEditObject(handle)) { + MTPE("object already open for edit in doBeginEditObject"); + return MTP_RESPONSE_GENERAL_ERROR; + } + + MtpString path; + int64_t fileLength; + MtpObjectFormat format; + MTPD("MtpServer::doBeginEditObject calling getObjectFilePath\n"); + mDatabase->lockMutex(); + int result = mDatabase->getObjectFilePath(handle, path, fileLength, format); + mDatabase->unlockMutex(); + if (result != MTP_RESPONSE_OK) + return result; + + int fd = open((const char *)path, O_RDWR | O_EXCL); + if (fd < 0) { + MTPE("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) { + MTPE("object not open for edit in doEndEditObject"); + return MTP_RESPONSE_GENERAL_ERROR; + } + + commitEdit(edit); + removeEditObject(handle); + return MTP_RESPONSE_OK; +} -- cgit v1.2.3