MtpServer.cpp revision 1865a5ddcfe7b0e8dc211419aea1094b1491a5fd
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, MtpDatabase* database,
117                    int fileGroup, int filePerm, int directoryPerm)
118    :   mFD(fd),
119        mDatabase(database),
120        mFileGroup(fileGroup),
121        mFilePermission(filePerm),
122        mDirectoryPermission(directoryPerm),
123        mSessionID(0),
124        mSessionOpen(false),
125        mSendObjectHandle(kInvalidObjectHandle),
126        mSendObjectFileSize(0)
127{
128    initObjectProperties();
129}
130
131MtpServer::~MtpServer() {
132}
133
134void MtpServer::addStorage(const char* filePath) {
135    int index = mStorages.size() + 1;
136    index |= index << 16;   // set high and low part to our index
137    MtpStorage* storage = new MtpStorage(index, filePath, mDatabase);
138    addStorage(storage);
139}
140
141MtpStorage* MtpServer::getStorage(MtpStorageID id) {
142    for (int i = 0; i < mStorages.size(); i++) {
143        MtpStorage* storage =  mStorages[i];
144        if (storage->getStorageID() == id)
145            return storage;
146    }
147    return NULL;
148}
149
150void MtpServer::scanStorage() {
151    for (int i = 0; i < mStorages.size(); i++) {
152        MtpStorage* storage =  mStorages[i];
153        storage->scanFiles();
154    }
155}
156
157void MtpServer::run() {
158    int fd = mFD;
159
160    LOGV("MtpServer::run fd: %d\n", fd);
161
162    while (1) {
163        int ret = mRequest.read(fd);
164        if (ret < 0) {
165            LOGE("request read returned %d, errno: %d", ret, errno);
166            if (errno == ECANCELED) {
167                // return to top of loop and wait for next command
168                continue;
169            }
170            break;
171        }
172        MtpOperationCode operation = mRequest.getOperationCode();
173        MtpTransactionID transaction = mRequest.getTransactionID();
174
175        LOGV("operation: %s", MtpDebug::getOperationCodeName(operation));
176        mRequest.dump();
177
178        // FIXME need to generalize this
179        bool dataIn = (operation == MTP_OPERATION_SEND_OBJECT_INFO);
180        if (dataIn) {
181            int ret = mData.read(fd);
182            if (ret < 0) {
183                LOGE("data read returned %d, errno: %d", ret, errno);
184                if (errno == ECANCELED) {
185                    // return to top of loop and wait for next command
186                    continue;
187                }
188                break;
189            }
190            LOGV("received data:");
191            mData.dump();
192        } else {
193            mData.reset();
194        }
195
196        if (handleRequest()) {
197            if (!dataIn && mData.hasData()) {
198                mData.setOperationCode(operation);
199                mData.setTransactionID(transaction);
200                LOGV("sending data:");
201                mData.dump();
202                ret = mData.write(fd);
203                if (ret < 0) {
204                    LOGE("request write returned %d, errno: %d", ret, errno);
205                    if (errno == ECANCELED) {
206                        // return to top of loop and wait for next command
207                        continue;
208                    }
209                    break;
210                }
211            }
212
213            mResponse.setTransactionID(transaction);
214            LOGV("sending response %04X", mResponse.getResponseCode());
215            ret = mResponse.write(fd);
216            if (ret < 0) {
217                LOGE("request write returned %d, errno: %d", ret, errno);
218                if (errno == ECANCELED) {
219                    // return to top of loop and wait for next command
220                    continue;
221                }
222                break;
223            }
224        } else {
225            LOGV("skipping response\n");
226        }
227    }
228}
229
230MtpProperty* MtpServer::getObjectProperty(MtpPropertyCode propCode) {
231    for (int i = 0; i < mObjectProperties.size(); i++) {
232        MtpProperty* property = mObjectProperties[i];
233        if (property->getPropertyCode() == propCode)
234            return property;
235    }
236    return NULL;
237}
238
239MtpProperty* MtpServer::getDeviceProperty(MtpPropertyCode propCode) {
240    for (int i = 0; i < mDeviceProperties.size(); i++) {
241        MtpProperty* property = mDeviceProperties[i];
242        if (property->getPropertyCode() == propCode)
243            return property;
244    }
245    return NULL;
246}
247
248void MtpServer::initObjectProperties() {
249    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_STORAGE_ID, MTP_TYPE_UINT16));
250    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_OBJECT_FORMAT, MTP_TYPE_UINT16));
251    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_OBJECT_SIZE, MTP_TYPE_UINT64));
252    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_OBJECT_FILE_NAME, MTP_TYPE_STR));
253    mObjectProperties.push(new MtpProperty(MTP_PROPERTY_PARENT_OBJECT, MTP_TYPE_UINT32));
254}
255
256bool MtpServer::handleRequest() {
257    MtpOperationCode operation = mRequest.getOperationCode();
258    MtpResponseCode response;
259
260    mResponse.reset();
261
262    if (mSendObjectHandle != kInvalidObjectHandle && operation != MTP_OPERATION_SEND_OBJECT) {
263        // FIXME - need to delete mSendObjectHandle from the database
264        LOGE("expected SendObject after SendObjectInfo");
265        mSendObjectHandle = kInvalidObjectHandle;
266    }
267
268    switch (operation) {
269        case MTP_OPERATION_GET_DEVICE_INFO:
270            response = doGetDeviceInfo();
271            break;
272        case MTP_OPERATION_OPEN_SESSION:
273            response = doOpenSession();
274            break;
275        case MTP_OPERATION_CLOSE_SESSION:
276            response = doCloseSession();
277            break;
278        case MTP_OPERATION_GET_STORAGE_IDS:
279            response = doGetStorageIDs();
280            break;
281         case MTP_OPERATION_GET_STORAGE_INFO:
282            response = doGetStorageInfo();
283            break;
284        case MTP_OPERATION_GET_OBJECT_PROPS_SUPPORTED:
285            response = doGetObjectPropsSupported();
286            break;
287        case MTP_OPERATION_GET_OBJECT_HANDLES:
288            response = doGetObjectHandles();
289            break;
290        case MTP_OPERATION_GET_OBJECT_PROP_VALUE:
291            response = doGetObjectPropValue();
292            break;
293        case MTP_OPERATION_GET_OBJECT_INFO:
294            response = doGetObjectInfo();
295            break;
296        case MTP_OPERATION_GET_OBJECT:
297            response = doGetObject();
298            break;
299        case MTP_OPERATION_SEND_OBJECT_INFO:
300            response = doSendObjectInfo();
301            break;
302        case MTP_OPERATION_SEND_OBJECT:
303            response = doSendObject();
304            break;
305        case MTP_OPERATION_DELETE_OBJECT:
306            response = doDeleteObject();
307            break;
308        case MTP_OPERATION_GET_OBJECT_PROP_DESC:
309            response = doGetObjectPropDesc();
310            break;
311        default:
312            response = MTP_RESPONSE_OPERATION_NOT_SUPPORTED;
313            break;
314    }
315
316    if (response == MTP_RESPONSE_TRANSACTION_CANCELLED)
317        return false;
318    mResponse.setResponseCode(response);
319    return true;
320}
321
322MtpResponseCode MtpServer::doGetDeviceInfo() {
323    MtpStringBuffer   string;
324    char prop_value[PROPERTY_VALUE_MAX];
325
326    // fill in device info
327    mData.putUInt16(MTP_STANDARD_VERSION);
328    mData.putUInt32(6); // MTP Vendor Extension ID
329    mData.putUInt16(MTP_STANDARD_VERSION);
330    string.set("microsoft.com: 1.0;");
331    mData.putString(string); // MTP Extensions
332    mData.putUInt16(0); //Functional Mode
333    mData.putAUInt16(kSupportedOperationCodes,
334            sizeof(kSupportedOperationCodes) / sizeof(uint16_t)); // Operations Supported
335    mData.putEmptyArray(); // Events Supported
336    mData.putEmptyArray(); // Device Properties Supported
337    mData.putEmptyArray(); // Capture Formats
338    mData.putAUInt16(kSupportedPlaybackFormats,
339            sizeof(kSupportedPlaybackFormats) / sizeof(uint16_t)); // Playback Formats
340    // FIXME
341    string.set("Google, Inc.");
342    mData.putString(string);   // Manufacturer
343
344    property_get("ro.product.model", prop_value, "MTP Device");
345    string.set(prop_value);
346    mData.putString(string);   // Model
347    string.set("1.0");
348    mData.putString(string);   // Device Version
349
350    property_get("ro.serialno", prop_value, "????????");
351    string.set(prop_value);
352    mData.putString(string);   // Serial Number
353
354    return MTP_RESPONSE_OK;
355}
356
357MtpResponseCode MtpServer::doOpenSession() {
358    if (mSessionOpen) {
359        mResponse.setParameter(1, mSessionID);
360        return MTP_RESPONSE_SESSION_ALREADY_OPEN;
361    }
362    mSessionID = mRequest.getParameter(1);
363    mSessionOpen = true;
364    return MTP_RESPONSE_OK;
365}
366
367MtpResponseCode MtpServer::doCloseSession() {
368    if (!mSessionOpen)
369        return MTP_RESPONSE_SESSION_NOT_OPEN;
370    mSessionID = 0;
371    mSessionOpen = false;
372    return MTP_RESPONSE_OK;
373}
374
375MtpResponseCode MtpServer::doGetStorageIDs() {
376    if (!mSessionOpen)
377        return MTP_RESPONSE_SESSION_NOT_OPEN;
378
379    int count = mStorages.size();
380    mData.putUInt32(count);
381    for (int i = 0; i < count; i++)
382        mData.putUInt32(mStorages[i]->getStorageID());
383
384    return MTP_RESPONSE_OK;
385}
386
387MtpResponseCode MtpServer::doGetStorageInfo() {
388    MtpStringBuffer   string;
389
390    if (!mSessionOpen)
391        return MTP_RESPONSE_SESSION_NOT_OPEN;
392    MtpStorageID id = mRequest.getParameter(1);
393    MtpStorage* storage = getStorage(id);
394    if (!storage)
395        return MTP_RESPONSE_INVALID_STORAGE_ID;
396
397    mData.putUInt16(storage->getType());
398    mData.putUInt16(storage->getFileSystemType());
399    mData.putUInt16(storage->getAccessCapability());
400    mData.putUInt64(storage->getMaxCapacity());
401    mData.putUInt64(storage->getFreeSpace());
402    mData.putUInt32(1024*1024*1024); // Free Space in Objects
403    string.set(storage->getDescription());
404    mData.putString(string);
405    mData.putEmptyString();   // Volume Identifier
406
407    return MTP_RESPONSE_OK;
408}
409
410MtpResponseCode MtpServer::doGetObjectPropsSupported() {
411    if (!mSessionOpen)
412        return MTP_RESPONSE_SESSION_NOT_OPEN;
413    MtpObjectFormat format = mRequest.getParameter(1);
414    mData.putAUInt16(kSupportedObjectProperties,
415            sizeof(kSupportedObjectProperties) / sizeof(uint16_t));
416    return MTP_RESPONSE_OK;
417}
418
419MtpResponseCode MtpServer::doGetObjectHandles() {
420    if (!mSessionOpen)
421        return MTP_RESPONSE_SESSION_NOT_OPEN;
422    MtpStorageID storageID = mRequest.getParameter(1);      // 0xFFFFFFFF for all storage
423    MtpObjectFormat format = mRequest.getParameter(2);      // 0 for all formats
424    MtpObjectHandle parent = mRequest.getParameter(3);      // 0xFFFFFFFF for objects with no parent
425                                                            // 0x00000000 for all objects?
426    if (parent == 0xFFFFFFFF)
427        parent = 0;
428
429    MtpObjectHandleList* handles = mDatabase->getObjectList(storageID, format, parent);
430    mData.putAUInt32(handles);
431    delete handles;
432    return MTP_RESPONSE_OK;
433}
434
435MtpResponseCode MtpServer::doGetObjectPropValue() {
436    MtpObjectHandle handle = mRequest.getParameter(1);
437    MtpObjectProperty property = mRequest.getParameter(2);
438
439    return mDatabase->getObjectProperty(handle, property, mData);
440}
441
442MtpResponseCode MtpServer::doGetObjectInfo() {
443    MtpObjectHandle handle = mRequest.getParameter(1);
444    return mDatabase->getObjectInfo(handle, mData);
445}
446
447MtpResponseCode MtpServer::doGetObject() {
448    MtpObjectHandle handle = mRequest.getParameter(1);
449    MtpString pathBuf;
450    int64_t fileLength;
451    if (!mDatabase->getObjectFilePath(handle, pathBuf, fileLength))
452        return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
453    const char* filePath = (const char *)pathBuf;
454
455    mtp_file_range  mfr;
456    mfr.fd = open(filePath, O_RDONLY);
457    if (mfr.fd < 0) {
458        return MTP_RESPONSE_GENERAL_ERROR;
459    }
460    mfr.offset = 0;
461    mfr.length = fileLength;
462
463    // send data header
464    mData.setOperationCode(mRequest.getOperationCode());
465    mData.setTransactionID(mRequest.getTransactionID());
466    mData.writeDataHeader(mFD, fileLength);
467
468    // then transfer the file
469    int ret = ioctl(mFD, MTP_SEND_FILE, (unsigned long)&mfr);
470    close(mfr.fd);
471    if (ret < 0) {
472        if (errno == ECANCELED)
473            return MTP_RESPONSE_TRANSACTION_CANCELLED;
474        else
475            return MTP_RESPONSE_GENERAL_ERROR;
476    }
477    return MTP_RESPONSE_OK;
478}
479
480MtpResponseCode MtpServer::doSendObjectInfo() {
481    MtpString path;
482    MtpStorageID storageID = mRequest.getParameter(1);
483    MtpStorage* storage = getStorage(storageID);
484    MtpObjectHandle parent = mRequest.getParameter(2);
485    if (!storage)
486        return MTP_RESPONSE_INVALID_STORAGE_ID;
487
488    // special case the root
489    if (parent == MTP_PARENT_ROOT) {
490        path = storage->getPath();
491        parent = 0;
492    } else {
493        int64_t dummy;
494        if (!mDatabase->getObjectFilePath(parent, path, dummy))
495            return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
496    }
497
498    // read only the fields we need
499    mData.getUInt32();  // storage ID
500    MtpObjectFormat format = mData.getUInt16();
501    mData.getUInt16();  // protection status
502    mSendObjectFileSize = mData.getUInt32();
503    mData.getUInt16();  // thumb format
504    mData.getUInt32();  // thumb compressed size
505    mData.getUInt32();  // thumb pix width
506    mData.getUInt32();  // thumb pix height
507    mData.getUInt32();  // image pix width
508    mData.getUInt32();  // image pix height
509    mData.getUInt32();  // image bit depth
510    mData.getUInt32();  // parent
511    uint16_t associationType = mData.getUInt16();
512    uint32_t associationDesc = mData.getUInt32();   // association desc
513    mData.getUInt32();  // sequence number
514    MtpStringBuffer name, created, modified;
515    mData.getString(name);    // file name
516    mData.getString(created);      // date created
517    mData.getString(modified);     // date modified
518    // keywords follow
519
520    time_t modifiedTime;
521    if (!parseDateTime(modified, modifiedTime))
522        modifiedTime = 0;
523
524    if (path[path.size() - 1] != '/')
525        path += "/";
526    path += (const char *)name;
527
528    mDatabase->beginTransaction();
529    MtpObjectHandle handle = mDatabase->addFile((const char*)path, format, parent, storageID,
530                                    mSendObjectFileSize, modifiedTime);
531    if (handle == kInvalidObjectHandle) {
532        mDatabase->rollbackTransaction();
533        return MTP_RESPONSE_GENERAL_ERROR;
534    }
535    mDatabase->commitTransaction();
536
537  if (format == MTP_FORMAT_ASSOCIATION) {
538        mode_t mask = umask(0);
539        int ret = mkdir((const char *)path, mDirectoryPermission);
540        umask(mask);
541        if (ret && ret != -EEXIST)
542            return MTP_RESPONSE_GENERAL_ERROR;
543        chown((const char *)path, getuid(), mFileGroup);
544    } else {
545        mSendObjectFilePath = path;
546        // save the handle for the SendObject call, which should follow
547        mSendObjectHandle = handle;
548    }
549
550    mResponse.setParameter(1, storageID);
551    mResponse.setParameter(2, (parent == 0 ? 0xFFFFFFFF: parent));
552    mResponse.setParameter(3, handle);
553
554    return MTP_RESPONSE_OK;
555}
556
557MtpResponseCode MtpServer::doSendObject() {
558    if (mSendObjectHandle == kInvalidObjectHandle) {
559        LOGE("Expected SendObjectInfo before SendObject");
560        return MTP_RESPONSE_NO_VALID_OBJECT_INFO;
561    }
562
563    // read the header
564    int ret = mData.readDataHeader(mFD);
565    // FIXME - check for errors here.
566
567    // reset so we don't attempt to send this back
568    mData.reset();
569
570    mtp_file_range  mfr;
571    mfr.fd = open(mSendObjectFilePath, O_RDWR | O_CREAT | O_TRUNC);
572    if (mfr.fd < 0) {
573        return MTP_RESPONSE_GENERAL_ERROR;
574    }
575    fchown(mfr.fd, getuid(), mFileGroup);
576    // set permissions
577    mode_t mask = umask(0);
578    fchmod(mfr.fd, mFilePermission);
579    umask(mask);
580
581    mfr.offset = 0;
582    mfr.length = mSendObjectFileSize;
583
584    // transfer the file
585    ret = ioctl(mFD, MTP_RECEIVE_FILE, (unsigned long)&mfr);
586    close(mfr.fd);
587
588    // FIXME - we need to delete mSendObjectHandle from the database if this fails.
589    LOGV("MTP_RECEIVE_FILE returned %d", ret);
590    mSendObjectHandle = kInvalidObjectHandle;
591
592    if (ret < 0) {
593        unlink(mSendObjectFilePath);
594        if (errno == ECANCELED)
595            return MTP_RESPONSE_TRANSACTION_CANCELLED;
596        else
597            return MTP_RESPONSE_GENERAL_ERROR;
598    }
599    return MTP_RESPONSE_OK;
600}
601
602MtpResponseCode MtpServer::doDeleteObject() {
603    MtpObjectHandle handle = mRequest.getParameter(1);
604    MtpObjectFormat format = mRequest.getParameter(1);
605    // FIXME - support deleting all objects if handle is 0xFFFFFFFF
606    // FIXME - implement deleting objects by format
607    // FIXME - handle non-empty directories
608
609    MtpString filePath;
610    int64_t fileLength;
611    if (!mDatabase->getObjectFilePath(handle, filePath, fileLength))
612        return MTP_RESPONSE_INVALID_OBJECT_HANDLE;
613
614    LOGV("deleting %s", (const char *)filePath);
615    // one of these should work
616    rmdir((const char *)filePath);
617    unlink((const char *)filePath);
618
619    mDatabase->deleteFile(handle);
620
621    return MTP_RESPONSE_OK;
622}
623
624MtpResponseCode MtpServer::doGetObjectPropDesc() {
625    MtpObjectProperty propCode = mRequest.getParameter(1);
626    MtpObjectFormat format = mRequest.getParameter(2);
627    MtpProperty* property = getObjectProperty(propCode);
628    if (!property)
629        return MTP_RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
630
631    property->write(mData);
632    return MTP_RESPONSE_OK;
633}
634
635}  // namespace android
636