MtpServer.cpp revision dda7e2b7378755637f188cca7c5ae854427a28f7
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include <stdio.h>
18#include <stdlib.h>
19#include <sys/types.h>
20#include <sys/ioctl.h>
21#include <sys/stat.h>
22#include <fcntl.h>
23#include <errno.h>
24
25#include <cutils/properties.h>
26
27#include "MtpDebug.h"
28#include "MtpProperty.h"
29#include "MtpServer.h"
30#include "MtpSqliteDatabase.h"
31#include "MtpStorage.h"
32#include "MtpStringBuffer.h"
33
34#include "f_mtp.h"
35
36namespace android {
37
38static const MtpOperationCode kSupportedOperationCodes[] = {
39    MTP_OPERATION_GET_DEVICE_INFO,
40    MTP_OPERATION_OPEN_SESSION,
41    MTP_OPERATION_CLOSE_SESSION,
42    MTP_OPERATION_GET_STORAGE_IDS,
43    MTP_OPERATION_GET_STORAGE_INFO,
44    MTP_OPERATION_GET_NUM_OBJECTS,
45    MTP_OPERATION_GET_OBJECT_HANDLES,
46    MTP_OPERATION_GET_OBJECT_INFO,
47    MTP_OPERATION_GET_OBJECT,
48//    MTP_OPERATION_GET_THUMB,
49    MTP_OPERATION_DELETE_OBJECT,
50    MTP_OPERATION_SEND_OBJECT_INFO,
51    MTP_OPERATION_SEND_OBJECT,
52//    MTP_OPERATION_INITIATE_CAPTURE,
53//    MTP_OPERATION_FORMAT_STORE,
54//    MTP_OPERATION_RESET_DEVICE,
55//    MTP_OPERATION_SELF_TEST,
56//    MTP_OPERATION_SET_OBJECT_PROTECTION,
57//    MTP_OPERATION_POWER_DOWN,
58    MTP_OPERATION_GET_DEVICE_PROP_DESC,
59    MTP_OPERATION_GET_DEVICE_PROP_VALUE,
60    MTP_OPERATION_SET_DEVICE_PROP_VALUE,
61    MTP_OPERATION_RESET_DEVICE_PROP_VALUE,
62//    MTP_OPERATION_TERMINATE_OPEN_CAPTURE,
63//    MTP_OPERATION_MOVE_OBJECT,
64//    MTP_OPERATION_COPY_OBJECT,
65//    MTP_OPERATION_GET_PARTIAL_OBJECT,
66//    MTP_OPERATION_INITIATE_OPEN_CAPTURE,
67    MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED,
68//    MTP_OPERATION_GET_OBJECT_PROP_DESC,
69    MTP_OPERATION_GET_OBJECT_PROP_VALUE,
70    MTP_OPERATION_SET_OBJECT_PROP_VALUE,
71//    MTP_OPERATION_GET_OBJECT_REFERENCES,
72//    MTP_OPERATION_SET_OBJECT_REFERENCES,
73//    MTP_OPERATION_SKIP,
74};
75
76static const MtpObjectProperty kSupportedObjectProperties[] = {
77    MTP_PROPERTY_STORAGE_ID,
78    MTP_PROPERTY_OBJECT_FORMAT,
79    MTP_PROPERTY_OBJECT_SIZE,
80    MTP_PROPERTY_OBJECT_FILE_NAME,
81    MTP_PROPERTY_PARENT_OBJECT,
82};
83
84static const MtpObjectFormat kSupportedPlaybackFormats[] = {
85    // MTP_FORMAT_UNDEFINED,
86    MTP_FORMAT_ASSOCIATION,
87    // MTP_FORMAT_TEXT,
88    // MTP_FORMAT_HTML,
89    MTP_FORMAT_MP3,
90    //MTP_FORMAT_AVI,
91    MTP_FORMAT_MPEG,
92    // MTP_FORMAT_ASF,
93    MTP_FORMAT_EXIF_JPEG,
94    MTP_FORMAT_TIFF_EP,
95    // MTP_FORMAT_BMP,
96    MTP_FORMAT_GIF,
97    MTP_FORMAT_JFIF,
98    MTP_FORMAT_PNG,
99    MTP_FORMAT_TIFF,
100    MTP_FORMAT_WMA,
101    MTP_FORMAT_OGG,
102    MTP_FORMAT_AAC,
103    // MTP_FORMAT_FLAC,
104    // MTP_FORMAT_WMV,
105    MTP_FORMAT_MP4_CONTAINER,
106    MTP_FORMAT_MP2,
107    MTP_FORMAT_3GP_CONTAINER,
108    // MTP_FORMAT_ABSTRACT_AUDIO_ALBUM,
109    // MTP_FORMAT_ABSTRACT_AV_PLAYLIST,
110    // MTP_FORMAT_WPL_PLAYLIST,
111    // MTP_FORMAT_M3U_PLAYLIST,
112    // MTP_FORMAT_MPL_PLAYLIST,
113    // MTP_FORMAT_PLS_PLAYLIST,
114};
115
116MtpServer::MtpServer(int fd, const char* databasePath,
117                    int fileGroup, int filePerm, int directoryPerm)
118    :   mFD(fd),
119        mDatabasePath(databasePath),
120        mDatabase(NULL),
121        mFileGroup(fileGroup),
122        mFilePermission(filePerm),
123        mDirectoryPermission(directoryPerm),
124        mSessionID(0),
125        mSessionOpen(false),
126        mSendObjectHandle(kInvalidObjectHandle),
127        mSendObjectFileSize(0)
128{
129    mDatabase = new MtpSqliteDatabase();
130    mDatabase->open(databasePath, true);
131
132    initObjectProperties();
133}
134
135MtpServer::~MtpServer() {
136}
137
138void MtpServer::addStorage(const char* filePath) {
139    int index = mStorages.size() + 1;
140    index |= index << 16;   // set high and low part to our index
141    MtpStorage* storage = new MtpStorage(index, filePath, mDatabase);
142    addStorage(storage);
143}
144
145MtpStorage* MtpServer::getStorage(MtpStorageID id) {
146    for (int i = 0; i < mStorages.size(); i++) {
147        MtpStorage* storage =  mStorages[i];
148        if (storage->getStorageID() == id)
149            return storage;
150    }
151    return NULL;
152}
153
154void MtpServer::scanStorage() {
155    for (int i = 0; i < mStorages.size(); i++) {
156        MtpStorage* storage =  mStorages[i];
157        storage->scanFiles();
158    }
159}
160
161void MtpServer::run() {
162    int fd = mFD;
163
164    LOGV("MtpServer::run fd: %d\n", fd);
165
166    while (1) {
167        int ret = mRequest.read(fd);
168        if (ret < 0) {
169            LOGE("request read returned %d, errno: %d", ret, errno);
170            if (errno == ECANCELED) {
171                // return to top of loop and wait for next command
172                continue;
173            }
174            break;
175        }
176        MtpOperationCode operation = mRequest.getOperationCode();
177        MtpTransactionID transaction = mRequest.getTransactionID();
178
179        LOGV("operation: %s", MtpDebug::getOperationCodeName(operation));
180        mRequest.dump();
181
182        // FIXME need to generalize this
183        bool dataIn = (operation == MTP_OPERATION_SEND_OBJECT_INFO);
184        if (dataIn) {
185            int ret = mData.read(fd);
186            if (ret < 0) {
187                LOGE("data read returned %d, errno: %d", ret, errno);
188                if (errno == ECANCELED) {
189                    // return to top of loop and wait for next command
190                    continue;
191                }
192                break;
193            }
194            LOGV("received data:");
195            mData.dump();
196        } else {
197            mData.reset();
198        }
199
200        if (handleRequest()) {
201            if (!dataIn && mData.hasData()) {
202                mData.setOperationCode(operation);
203                mData.setTransactionID(transaction);
204                LOGV("sending data:");
205                mData.dump();
206                ret = mData.write(fd);
207                if (ret < 0) {
208                    LOGE("request write returned %d, errno: %d", ret, errno);
209                    if (errno == ECANCELED) {
210                        // return to top of loop and wait for next command
211                        continue;
212                    }
213                    break;
214                }
215            }
216
217            mResponse.setTransactionID(transaction);
218            LOGV("sending response %04X", mResponse.getResponseCode());
219            ret = mResponse.write(fd);
220            if (ret < 0) {
221                LOGE("request write returned %d, errno: %d", ret, errno);
222                if (errno == ECANCELED) {
223                    // return to top of loop and wait for next command
224                    continue;
225                }
226                break;
227            }
228        } else {
229            LOGV("skipping response\n");
230        }
231    }
232}
233
234MtpProperty* MtpServer::getObjectProperty(MtpPropertyCode propCode) {
235    for (int i = 0; i < mObjectProperties.size(); i++) {
236        MtpProperty* property = mObjectProperties[i];
237        if (property->getPropertyCode() == propCode)
238            return property;
239    }
240    return NULL;
241}
242
243MtpProperty* MtpServer::getDeviceProperty(MtpPropertyCode propCode) {
244    for (int i = 0; i < mDeviceProperties.size(); i++) {
245        MtpProperty* property = mDeviceProperties[i];
246        if (property->getPropertyCode() == propCode)
247            return property;
248    }
249    return NULL;
250}
251
252void MtpServer::initObjectProperties() {
253    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_STORAGE_ID, MTP_TYPE_UINT16));
254    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_OBJECT_FORMAT, MTP_TYPE_UINT16));
255    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_OBJECT_SIZE, MTP_TYPE_UINT64));
256    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_OBJECT_FILE_NAME, MTP_TYPE_STR));
257    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_PARENT_OBJECT, MTP_TYPE_UINT32));
258}
259
260bool MtpServer::handleRequest() {
261    MtpOperationCode operation = mRequest.getOperationCode();
262    MtpResponseCode response;
263
264    mResponse.reset();
265
266    if (mSendObjectHandle != kInvalidObjectHandle && operation != MTP_OPERATION_SEND_OBJECT) {
267        // FIXME - need to delete mSendObjectHandle from the database
268        LOGE("expected SendObject after SendObjectInfo");
269        mSendObjectHandle = kInvalidObjectHandle;
270    }
271
272    switch (operation) {
273        case MTP_OPERATION_GET_DEVICE_INFO:
274            response = doGetDeviceInfo();
275            break;
276        case MTP_OPERATION_OPEN_SESSION:
277            response = doOpenSession();
278            break;
279        case MTP_OPERATION_CLOSE_SESSION:
280            response = doCloseSession();
281            break;
282        case MTP_OPERATION_GET_STORAGE_IDS:
283            response = doGetStorageIDs();
284            break;
285         case MTP_OPERATION_GET_STORAGE_INFO:
286            response = doGetStorageInfo();
287            break;
288        case MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED:
289            response = doGetObjectPropsSupported();
290            break;
291        case MTP_OPERATION_GET_OBJECT_HANDLES:
292            response = doGetObjectHandles();
293            break;
294        case MTP_OPERATION_GET_OBJECT_PROP_VALUE:
295            response = doGetObjectPropValue();
296            break;
297        case MTP_OPERATION_GET_OBJECT_INFO:
298            response = doGetObjectInfo();
299            break;
300        case MTP_OPERATION_GET_OBJECT:
301            response = doGetObject();
302            break;
303        case MTP_OPERATION_SEND_OBJECT_INFO:
304            response = doSendObjectInfo();
305            break;
306        case MTP_OPERATION_SEND_OBJECT:
307            response = doSendObject();
308            break;
309        case MTP_OPERATION_DELETE_OBJECT:
310            response = doDeleteObject();
311            break;
312        case MTP_OPERATION_GET_OBJECT_PROP_DESC:
313            response = doGetObjectPropDesc();
314            break;
315        default:
316            response = MTP_RESPONSE_OPERATION_NOT_SUPPORTED;
317            break;
318    }
319
320    if (response == MTP_RESPONSE_TRANSACTION_CANCELLED)
321        return false;
322    mResponse.setResponseCode(response);
323    return true;
324}
325
326MtpResponseCode MtpServer::doGetDeviceInfo() {
327    MtpStringBuffer   string;
328    char prop_value[PROPERTY_VALUE_MAX];
329
330    // fill in device info
331    mData.putUInt16(MTP_STANDARD_VERSION);
332    mData.putUInt32(6); // MTP Vendor Extension ID
333    mData.putUInt16(MTP_STANDARD_VERSION);
334    string.set("microsoft.com: 1.0;");
335    mData.putString(string); // MTP Extensions
336    mData.putUInt16(0); //Functional Mode
337    mData.putAUInt16(kSupportedOperationCodes,
338            sizeof(kSupportedOperationCodes) / sizeof(uint16_t)); // Operations Supported
339    mData.putEmptyArray(); // Events Supported
340    mData.putEmptyArray(); // Device Properties Supported
341    mData.putEmptyArray(); // Capture Formats
342    mData.putAUInt16(kSupportedPlaybackFormats,
343            sizeof(kSupportedPlaybackFormats) / sizeof(uint16_t)); // Playback Formats
344    // FIXME
345    string.set("Google, Inc.");
346    mData.putString(string);   // Manufacturer
347
348    property_get("ro.product.model", prop_value, "MTP Device");
349    string.set(prop_value);
350    mData.putString(string);   // Model
351    string.set("1.0");
352    mData.putString(string);   // Device Version
353
354    property_get("ro.serialno", prop_value, "????????");
355    string.set(prop_value);
356    mData.putString(string);   // Serial Number
357
358    return MTP_RESPONSE_OK;
359}
360
361MtpResponseCode MtpServer::doOpenSession() {
362    if (mSessionOpen) {
363        mResponse.setParameter(1, mSessionID);
364        return MTP_RESPONSE_SESSION_ALREADY_OPEN;
365    }
366    mSessionID = mRequest.getParameter(1);
367    mSessionOpen = true;
368    return MTP_RESPONSE_OK;
369}
370
371MtpResponseCode MtpServer::doCloseSession() {
372    if (!mSessionOpen)
373        return MTP_RESPONSE_SESSION_NOT_OPEN;
374    mSessionID = 0;
375    mSessionOpen = false;
376    return MTP_RESPONSE_OK;
377}
378
379MtpResponseCode MtpServer::doGetStorageIDs() {
380    if (!mSessionOpen)
381        return MTP_RESPONSE_SESSION_NOT_OPEN;
382
383    int count = mStorages.size();
384    mData.putUInt32(count);
385    for (int i = 0; i < count; i++)
386        mData.putUInt32(mStorages[i]->getStorageID());
387
388    return MTP_RESPONSE_OK;
389}
390
391MtpResponseCode MtpServer::doGetStorageInfo() {
392    MtpStringBuffer   string;
393
394    if (!mSessionOpen)
395        return MTP_RESPONSE_SESSION_NOT_OPEN;
396    MtpStorageID id = mRequest.getParameter(1);
397    MtpStorage* storage = getStorage(id);
398    if (!storage)
399        return MTP_RESPONSE_INVALID_STORAGE_ID;
400
401    mData.putUInt16(storage->getType());
402    mData.putUInt16(storage->getFileSystemType());
403    mData.putUInt16(storage->getAccessCapability());
404    mData.putUInt64(storage->getMaxCapacity());
405    mData.putUInt64(storage->getFreeSpace());
406    mData.putUInt32(1024*1024*1024); // Free Space in Objects
407    string.set(storage->getDescription());
408    mData.putString(string);
409    mData.putEmptyString();   // Volume Identifier
410
411    return MTP_RESPONSE_OK;
412}
413
414MtpResponseCode MtpServer::doGetObjectPropsSupported() {
415    if (!mSessionOpen)
416        return MTP_RESPONSE_SESSION_NOT_OPEN;
417    MtpObjectFormat format = mRequest.getParameter(1);
418    mData.putAUInt16(kSupportedObjectProperties,
419            sizeof(kSupportedObjectProperties) / sizeof(uint16_t));
420    return MTP_RESPONSE_OK;
421}
422
423MtpResponseCode MtpServer::doGetObjectHandles() {
424    if (!mSessionOpen)
425        return MTP_RESPONSE_SESSION_NOT_OPEN;
426    MtpStorageID storageID = mRequest.getParameter(1);      // 0xFFFFFFFF for all storage
427    MtpObjectFormat format = mRequest.getParameter(2);      // 0 for all formats
428    MtpObjectHandle parent = mRequest.getParameter(3);      // 0xFFFFFFFF for objects with no parent
429                                                            // 0x00000000 for all objects?
430
431    MtpObjectHandleList* handles = mDatabase->getObjectList(storageID, format, parent);
432    mData.putAUInt32(handles);
433    delete handles;
434    return MTP_RESPONSE_OK;
435}
436
437MtpResponseCode MtpServer::doGetObjectPropValue() {
438    MtpObjectHandle handle = mRequest.getParameter(1);
439    MtpObjectProperty property = mRequest.getParameter(2);
440
441    return mDatabase->getObjectProperty(handle, property, mData);
442}
443
444MtpResponseCode MtpServer::doGetObjectInfo() {
445    MtpObjectHandle handle = mRequest.getParameter(1);
446    return mDatabase->getObjectInfo(handle, mData);
447}
448
449MtpResponseCode MtpServer::doGetObject() {
450    MtpObjectHandle handle = mRequest.getParameter(1);
451    MtpString pathBuf;
452    int64_t fileLength;
453    if (!mDatabase->getObjectFilePath(handle, pathBuf, fileLength))
454        return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
455    const char* filePath = (const char *)pathBuf;
456
457    mtp_file_range  mfr;
458    mfr.fd = open(filePath, O_RDONLY);
459    if (mfr.fd < 0) {
460        return MTP_RESPONSE_GENERAL_ERROR;
461    }
462    mfr.offset = 0;
463    mfr.length = fileLength;
464
465    // send data header
466    mData.setOperationCode(mRequest.getOperationCode());
467    mData.setTransactionID(mRequest.getTransactionID());
468    mData.writeDataHeader(mFD, fileLength);
469
470    // then transfer the file
471    int ret = ioctl(mFD, MTP_SEND_FILE, (unsigned long)&mfr);
472    close(mfr.fd);
473    if (ret < 0) {
474        if (errno == ECANCELED)
475            return MTP_RESPONSE_TRANSACTION_CANCELLED;
476        else
477            return MTP_RESPONSE_GENERAL_ERROR;
478    }
479    return MTP_RESPONSE_OK;
480}
481
482MtpResponseCode MtpServer::doSendObjectInfo() {
483    MtpString path;
484    MtpStorageID storageID = mRequest.getParameter(1);
485    MtpStorage* storage = getStorage(storageID);
486    MtpObjectHandle parent = mRequest.getParameter(2);
487    if (!storage)
488        return MTP_RESPONSE_INVALID_STORAGE_ID;
489
490    // special case the root
491    if (parent == MTP_PARENT_ROOT)
492        path = storage->getPath();
493    else {
494        int64_t dummy;
495        if (!mDatabase->getObjectFilePath(parent, path, dummy))
496            return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
497    }
498
499    // read only the fields we need
500    mData.getUInt32();  // storage ID
501    MtpObjectFormat format = mData.getUInt16();
502    mData.getUInt16();  // protection status
503    mSendObjectFileSize = mData.getUInt32();
504    mData.getUInt16();  // thumb format
505    mData.getUInt32();  // thumb compressed size
506    mData.getUInt32();  // thumb pix width
507    mData.getUInt32();  // thumb pix height
508    mData.getUInt32();  // image pix width
509    mData.getUInt32();  // image pix height
510    mData.getUInt32();  // image bit depth
511    mData.getUInt32();  // parent
512    uint16_t associationType = mData.getUInt16();
513    uint32_t associationDesc = mData.getUInt32();   // association desc
514    mData.getUInt32();  // sequence number
515    MtpStringBuffer name, created, modified;
516    mData.getString(name);    // file name
517    mData.getString(created);      // date created
518    mData.getString(modified);     // date modified
519    // keywords follow
520
521    time_t modifiedTime;
522    if (!parseDateTime(modified, modifiedTime))
523        modifiedTime = 0;
524
525    if (path[path.size() - 1] != '/')
526        path += "/";
527    path += (const char *)name;
528
529    mDatabase->beginTransaction();
530    MtpObjectHandle handle = mDatabase->addFile((const char*)path, format, parent, storageID,
531                                    mSendObjectFileSize, modifiedTime);
532    if (handle == kInvalidObjectHandle) {
533        mDatabase->rollbackTransaction();
534        return MTP_RESPONSE_GENERAL_ERROR;
535    }
536    mDatabase->commitTransaction();
537
538  if (format == MTP_FORMAT_ASSOCIATION) {
539        mode_t mask = umask(0);
540        int ret = mkdir((const char *)path, mDirectoryPermission);
541        umask(mask);
542        if (ret && ret != -EEXIST)
543            return MTP_RESPONSE_GENERAL_ERROR;
544        chown((const char *)path, getuid(), mFileGroup);
545    } else {
546        mSendObjectFilePath = path;
547        // save the handle for the SendObject call, which should follow
548        mSendObjectHandle = handle;
549    }
550
551    mResponse.setParameter(1, storageID);
552    mResponse.setParameter(2, parent);
553    mResponse.setParameter(3, handle);
554
555    return MTP_RESPONSE_OK;
556}
557
558MtpResponseCode MtpServer::doSendObject() {
559    if (mSendObjectHandle == kInvalidObjectHandle) {
560        LOGE("Expected SendObjectInfo before SendObject");
561        return MTP_RESPONSE_NO_VALID_OBJECT_INFO;
562    }
563
564    // read the header
565    int ret = mData.readDataHeader(mFD);
566    // FIXME - check for errors here.
567
568    // reset so we don't attempt to send this back
569    mData.reset();
570
571    mtp_file_range  mfr;
572    mfr.fd = open(mSendObjectFilePath, O_RDWR | O_CREAT | O_TRUNC);
573    if (mfr.fd < 0) {
574        return MTP_RESPONSE_GENERAL_ERROR;
575    }
576    fchown(mfr.fd, getuid(), mFileGroup);
577    // set permissions
578    mode_t mask = umask(0);
579    fchmod(mfr.fd, mFilePermission);
580    umask(mask);
581
582    mfr.offset = 0;
583    mfr.length = mSendObjectFileSize;
584
585    // transfer the file
586    ret = ioctl(mFD, MTP_RECEIVE_FILE, (unsigned long)&mfr);
587    close(mfr.fd);
588
589    // FIXME - we need to delete mSendObjectHandle from the database if this fails.
590    LOGV("MTP_RECEIVE_FILE returned %d", ret);
591    mSendObjectHandle = kInvalidObjectHandle;
592
593    if (ret < 0) {
594        unlink(mSendObjectFilePath);
595        if (errno == ECANCELED)
596            return MTP_RESPONSE_TRANSACTION_CANCELLED;
597        else
598            return MTP_RESPONSE_GENERAL_ERROR;
599    }
600    return MTP_RESPONSE_OK;
601}
602
603MtpResponseCode MtpServer::doDeleteObject() {
604    MtpObjectHandle handle = mRequest.getParameter(1);
605    MtpObjectFormat format = mRequest.getParameter(1);
606    // FIXME - support deleting all objects if handle is 0xFFFFFFFF
607    // FIXME - implement deleting objects by format
608    // FIXME - handle non-empty directories
609
610    MtpString filePath;
611    int64_t fileLength;
612    if (!mDatabase->getObjectFilePath(handle, filePath, fileLength))
613        return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
614
615    LOGV("deleting %s", (const char *)filePath);
616    // one of these should work
617    rmdir((const char *)filePath);
618    unlink((const char *)filePath);
619
620    mDatabase->deleteFile(handle);
621
622    return MTP_RESPONSE_OK;
623}
624
625MtpResponseCode MtpServer::doGetObjectPropDesc() {
626    MtpObjectProperty propCode = mRequest.getParameter(1);
627    MtpObjectFormat format = mRequest.getParameter(2);
628    MtpProperty* property = getObjectProperty(propCode);
629    if (!property)
630        return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
631
632    property->write(mData);
633    return MTP_RESPONSE_OK;
634}
635
636}  // namespace android
637