MtpDatabase.java revision 0cd0136d440cf6ad9d5fab430269116786e671ec
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
17package android.mtp;
18
19import android.content.Context;
20import android.content.ContentValues;
21import android.content.IContentProvider;
22import android.content.Intent;
23import android.database.Cursor;
24import android.database.sqlite.SQLiteDatabase;
25import android.media.MediaScanner;
26import android.net.Uri;
27import android.os.Environment;
28import android.os.RemoteException;
29import android.provider.MediaStore;
30import android.provider.MediaStore.Audio;
31import android.provider.MediaStore.Files;
32import android.provider.MediaStore.Images;
33import android.provider.MediaStore.MediaColumns;
34import android.util.Log;
35import android.view.Display;
36import android.view.WindowManager;
37
38import java.io.File;
39import java.util.HashMap;
40
41/**
42 * {@hide}
43 */
44public class MtpDatabase {
45
46    private static final String TAG = "MtpDatabase";
47
48    private final Context mContext;
49    private final IContentProvider mMediaProvider;
50    private final String mVolumeName;
51    private final Uri mObjectsUri;
52    private final String mMediaStoragePath;
53    private final String mExternalStoragePath;
54
55    // cached property groups for single properties
56    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty
57            = new HashMap<Integer, MtpPropertyGroup>();
58
59    // cached property groups for all properties for a given format
60    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat
61            = new HashMap<Integer, MtpPropertyGroup>();
62
63    // true if the database has been modified in the current MTP session
64    private boolean mDatabaseModified;
65
66    // database for writable MTP device properties
67    private SQLiteDatabase mDevicePropDb;
68    private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1;
69
70    // FIXME - this should be passed in via the constructor
71    private final int mStorageID = 0x00010001;
72
73    private static final String[] ID_PROJECTION = new String[] {
74            Files.FileColumns._ID, // 0
75    };
76    private static final String[] PATH_PROJECTION = new String[] {
77            Files.FileColumns._ID, // 0
78            Files.FileColumns.DATA, // 1
79    };
80    private static final String[] PATH_SIZE_FORMAT_PROJECTION = new String[] {
81            Files.FileColumns._ID, // 0
82            Files.FileColumns.DATA, // 1
83            Files.FileColumns.SIZE, // 2
84            Files.FileColumns.FORMAT, // 3
85    };
86    private static final String[] OBJECT_INFO_PROJECTION = new String[] {
87            Files.FileColumns._ID, // 0
88            Files.FileColumns.DATA, // 1
89            Files.FileColumns.FORMAT, // 2
90            Files.FileColumns.PARENT, // 3
91            Files.FileColumns.SIZE, // 4
92            Files.FileColumns.DATE_MODIFIED, // 5
93    };
94    private static final String ID_WHERE = Files.FileColumns._ID + "=?";
95    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
96    private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
97    private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND "
98                                            + Files.FileColumns.FORMAT + "=?";
99
100    private static final String[] DEVICE_PROPERTY_PROJECTION = new String[] { "_id", "value" };
101    private  static final String DEVICE_PROPERTY_WHERE = "code=?";
102
103    private final MediaScanner mMediaScanner;
104
105    static {
106        System.loadLibrary("media_jni");
107    }
108
109    public MtpDatabase(Context context, String volumeName, String storagePath) {
110        native_setup();
111
112        mContext = context;
113        mMediaProvider = context.getContentResolver().acquireProvider("media");
114        mVolumeName = volumeName;
115        mMediaStoragePath = storagePath;
116        mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath();
117        mObjectsUri = Files.getMtpObjectsUri(volumeName);
118        mMediaScanner = new MediaScanner(context);
119        openDevicePropertiesDatabase(context);
120    }
121
122    @Override
123    protected void finalize() throws Throwable {
124        try {
125            native_finalize();
126        } finally {
127            super.finalize();
128        }
129    }
130
131    private String externalToMediaPath(String path) {
132        // convert external storage path to media path
133        if (path != null && mMediaStoragePath != null
134                && mExternalStoragePath != null
135                && path.startsWith(mExternalStoragePath)) {
136            path = mMediaStoragePath + path.substring(mExternalStoragePath.length());
137        }
138        return path;
139    }
140
141    private void openDevicePropertiesDatabase(Context context) {
142        mDevicePropDb = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
143        int version = mDevicePropDb.getVersion();
144
145        // initialize if necessary
146        if (version != DEVICE_PROPERTIES_DATABASE_VERSION) {
147            mDevicePropDb.execSQL("CREATE TABLE properties (" +
148                    "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
149                    "code INTEGER UNIQUE ON CONFLICT REPLACE," +
150                    "value TEXT" +
151                    ");");
152            mDevicePropDb.execSQL("CREATE INDEX property_index ON properties (code);");
153            mDevicePropDb.setVersion(DEVICE_PROPERTIES_DATABASE_VERSION);
154        }
155    }
156
157    private int beginSendObject(String path, int format, int parent,
158                         int storage, long size, long modified) {
159        // first make sure the object does not exist
160        if (path != null) {
161            Cursor c = null;
162            try {
163                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
164                        new String[] { path }, null);
165                if (c != null && c.getCount() > 0) {
166                    Log.w(TAG, "file already exists in beginSendObject: " + path);
167                    return -1;
168                }
169            } catch (RemoteException e) {
170                Log.e(TAG, "RemoteException in beginSendObject", e);
171            } finally {
172                if (c != null) {
173                    c.close();
174                }
175            }
176        }
177
178        mDatabaseModified = true;
179        ContentValues values = new ContentValues();
180        values.put(Files.FileColumns.DATA, path);
181        values.put(Files.FileColumns.FORMAT, format);
182        values.put(Files.FileColumns.PARENT, parent);
183        // storage is ignored for now
184        values.put(Files.FileColumns.SIZE, size);
185        values.put(Files.FileColumns.DATE_MODIFIED, modified);
186
187        try {
188            Uri uri = mMediaProvider.insert(mObjectsUri, values);
189            if (uri != null) {
190                return Integer.parseInt(uri.getPathSegments().get(2));
191            } else {
192                return -1;
193            }
194        } catch (RemoteException e) {
195            Log.e(TAG, "RemoteException in beginSendObject", e);
196            return -1;
197        }
198    }
199
200    private void endSendObject(String path, int handle, int format, long actualSize, boolean succeeded) {
201        if (succeeded) {
202            // handle abstract playlists separately
203            // they do not exist in the file system so don't use the media scanner here
204            if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
205                // extract name from path
206                String name = path;
207                int lastSlash = name.lastIndexOf('/');
208                if (lastSlash >= 0) {
209                    name = name.substring(lastSlash + 1);
210                }
211
212                ContentValues values = new ContentValues(1);
213                values.put(Audio.Playlists.DATA, path);
214                values.put(Audio.Playlists.NAME, name);
215                values.put(Files.FileColumns.FORMAT, format);
216                values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
217                try {
218                    Uri uri = mMediaProvider.insert(Audio.Playlists.EXTERNAL_CONTENT_URI, values);
219                } catch (RemoteException e) {
220                    Log.e(TAG, "RemoteException in endSendObject", e);
221                }
222            } else {
223                if (actualSize >= 0) {
224                    // update size if necessary
225                    ContentValues values = new ContentValues();
226                    values.put(Files.FileColumns.SIZE, actualSize);
227                    try {
228                        String[] whereArgs = new String[] {  Integer.toString(handle) };
229                        mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
230                    } catch (RemoteException e) {
231                        Log.e(TAG, "RemoteException in mMediaProvider.update", e);
232                    }
233                }
234
235                mMediaScanner.scanMtpFile(path, mVolumeName, handle, format);
236            }
237        } else {
238            deleteFile(handle);
239        }
240    }
241
242    private int[] getObjectList(int storageID, int format, int parent) {
243        // we can ignore storageID until we support multiple storages
244        Log.d(TAG, "getObjectList parent: " + parent);
245        Cursor c = null;
246        try {
247            if (format != 0) {
248                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
249                            PARENT_FORMAT_WHERE,
250                            new String[] { Integer.toString(parent), Integer.toString(format) },
251                             null);
252            } else {
253                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
254                            PARENT_WHERE, new String[] { Integer.toString(parent) }, null);
255            }
256            if (c == null) {
257                Log.d(TAG, "null cursor");
258                return null;
259            }
260            int count = c.getCount();
261            if (count > 0) {
262                int[] result = new int[count];
263                for (int i = 0; i < count; i++) {
264                    c.moveToNext();
265                    result[i] = c.getInt(0);
266                }
267                Log.d(TAG, "returning " + result);
268                return result;
269            }
270        } catch (RemoteException e) {
271            Log.e(TAG, "RemoteException in getObjectList", e);
272        } finally {
273            if (c != null) {
274                c.close();
275            }
276        }
277        return null;
278    }
279
280    private int getNumObjects(int storageID, int format, int parent) {
281        // we can ignore storageID until we support multiple storages
282        Log.d(TAG, "getObjectList parent: " + parent);
283        Cursor c = null;
284        try {
285            if (format != 0) {
286                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
287                            PARENT_FORMAT_WHERE,
288                            new String[] { Integer.toString(parent), Integer.toString(format) },
289                             null);
290            } else {
291                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
292                            PARENT_WHERE, new String[] { Integer.toString(parent) }, null);
293            }
294            if (c != null) {
295                return c.getCount();
296            }
297        } catch (RemoteException e) {
298            Log.e(TAG, "RemoteException in getNumObjects", e);
299        } finally {
300            if (c != null) {
301                c.close();
302            }
303        }
304        return -1;
305    }
306
307    private int[] getSupportedPlaybackFormats() {
308        return new int[] {
309            // allow transfering arbitrary files
310            MtpConstants.FORMAT_UNDEFINED,
311
312            MtpConstants.FORMAT_ASSOCIATION,
313            MtpConstants.FORMAT_TEXT,
314            MtpConstants.FORMAT_HTML,
315            MtpConstants.FORMAT_WAV,
316            MtpConstants.FORMAT_MP3,
317            MtpConstants.FORMAT_MPEG,
318            MtpConstants.FORMAT_EXIF_JPEG,
319            MtpConstants.FORMAT_TIFF_EP,
320            MtpConstants.FORMAT_GIF,
321            MtpConstants.FORMAT_JFIF,
322            MtpConstants.FORMAT_PNG,
323            MtpConstants.FORMAT_TIFF,
324            MtpConstants.FORMAT_WMA,
325            MtpConstants.FORMAT_OGG,
326            MtpConstants.FORMAT_AAC,
327            MtpConstants.FORMAT_MP4_CONTAINER,
328            MtpConstants.FORMAT_MP2,
329            MtpConstants.FORMAT_3GP_CONTAINER,
330            MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
331            MtpConstants.FORMAT_WPL_PLAYLIST,
332            MtpConstants.FORMAT_M3U_PLAYLIST,
333            MtpConstants.FORMAT_PLS_PLAYLIST,
334            MtpConstants.FORMAT_XML_DOCUMENT,
335        };
336    }
337
338    private int[] getSupportedCaptureFormats() {
339        // no capture formats yet
340        return null;
341    }
342
343    static final int[] FILE_PROPERTIES = {
344            // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
345            // and IMAGE_PROPERTIES below
346            MtpConstants.PROPERTY_STORAGE_ID,
347            MtpConstants.PROPERTY_OBJECT_FORMAT,
348            MtpConstants.PROPERTY_PROTECTION_STATUS,
349            MtpConstants.PROPERTY_OBJECT_SIZE,
350            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
351            MtpConstants.PROPERTY_DATE_MODIFIED,
352            MtpConstants.PROPERTY_PARENT_OBJECT,
353            MtpConstants.PROPERTY_PERSISTENT_UID,
354            MtpConstants.PROPERTY_NAME,
355            MtpConstants.PROPERTY_DATE_ADDED,
356    };
357
358    static final int[] AUDIO_PROPERTIES = {
359            // NOTE must match FILE_PROPERTIES above
360            MtpConstants.PROPERTY_STORAGE_ID,
361            MtpConstants.PROPERTY_OBJECT_FORMAT,
362            MtpConstants.PROPERTY_PROTECTION_STATUS,
363            MtpConstants.PROPERTY_OBJECT_SIZE,
364            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
365            MtpConstants.PROPERTY_DATE_MODIFIED,
366            MtpConstants.PROPERTY_PARENT_OBJECT,
367            MtpConstants.PROPERTY_PERSISTENT_UID,
368            MtpConstants.PROPERTY_NAME,
369            MtpConstants.PROPERTY_DISPLAY_NAME,
370            MtpConstants.PROPERTY_DATE_ADDED,
371
372            // audio specific properties
373            MtpConstants.PROPERTY_ARTIST,
374            MtpConstants.PROPERTY_ALBUM_NAME,
375            MtpConstants.PROPERTY_ALBUM_ARTIST,
376            MtpConstants.PROPERTY_TRACK,
377            MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
378            MtpConstants.PROPERTY_DURATION,
379            MtpConstants.PROPERTY_GENRE,
380            MtpConstants.PROPERTY_COMPOSER,
381    };
382
383    static final int[] VIDEO_PROPERTIES = {
384            // NOTE must match FILE_PROPERTIES above
385            MtpConstants.PROPERTY_STORAGE_ID,
386            MtpConstants.PROPERTY_OBJECT_FORMAT,
387            MtpConstants.PROPERTY_PROTECTION_STATUS,
388            MtpConstants.PROPERTY_OBJECT_SIZE,
389            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
390            MtpConstants.PROPERTY_DATE_MODIFIED,
391            MtpConstants.PROPERTY_PARENT_OBJECT,
392            MtpConstants.PROPERTY_PERSISTENT_UID,
393            MtpConstants.PROPERTY_NAME,
394            MtpConstants.PROPERTY_DISPLAY_NAME,
395            MtpConstants.PROPERTY_DATE_ADDED,
396
397            // video specific properties
398            MtpConstants.PROPERTY_ARTIST,
399            MtpConstants.PROPERTY_ALBUM_NAME,
400            MtpConstants.PROPERTY_DURATION,
401            MtpConstants.PROPERTY_DESCRIPTION,
402    };
403
404    static final int[] IMAGE_PROPERTIES = {
405            // NOTE must match FILE_PROPERTIES above
406            MtpConstants.PROPERTY_STORAGE_ID,
407            MtpConstants.PROPERTY_OBJECT_FORMAT,
408            MtpConstants.PROPERTY_PROTECTION_STATUS,
409            MtpConstants.PROPERTY_OBJECT_SIZE,
410            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
411            MtpConstants.PROPERTY_DATE_MODIFIED,
412            MtpConstants.PROPERTY_PARENT_OBJECT,
413            MtpConstants.PROPERTY_PERSISTENT_UID,
414            MtpConstants.PROPERTY_NAME,
415            MtpConstants.PROPERTY_DISPLAY_NAME,
416            MtpConstants.PROPERTY_DATE_ADDED,
417
418            // image specific properties
419            MtpConstants.PROPERTY_DESCRIPTION,
420    };
421
422    static final int[] ALL_PROPERTIES = {
423            // NOTE must match FILE_PROPERTIES above
424            MtpConstants.PROPERTY_STORAGE_ID,
425            MtpConstants.PROPERTY_OBJECT_FORMAT,
426            MtpConstants.PROPERTY_PROTECTION_STATUS,
427            MtpConstants.PROPERTY_OBJECT_SIZE,
428            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
429            MtpConstants.PROPERTY_DATE_MODIFIED,
430            MtpConstants.PROPERTY_PARENT_OBJECT,
431            MtpConstants.PROPERTY_PERSISTENT_UID,
432            MtpConstants.PROPERTY_NAME,
433            MtpConstants.PROPERTY_DISPLAY_NAME,
434            MtpConstants.PROPERTY_DATE_ADDED,
435
436            // image specific properties
437            MtpConstants.PROPERTY_DESCRIPTION,
438
439            // audio specific properties
440            MtpConstants.PROPERTY_ARTIST,
441            MtpConstants.PROPERTY_ALBUM_NAME,
442            MtpConstants.PROPERTY_ALBUM_ARTIST,
443            MtpConstants.PROPERTY_TRACK,
444            MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
445            MtpConstants.PROPERTY_DURATION,
446            MtpConstants.PROPERTY_GENRE,
447            MtpConstants.PROPERTY_COMPOSER,
448
449            // video specific properties
450            MtpConstants.PROPERTY_ARTIST,
451            MtpConstants.PROPERTY_ALBUM_NAME,
452            MtpConstants.PROPERTY_DURATION,
453            MtpConstants.PROPERTY_DESCRIPTION,
454
455            // image specific properties
456            MtpConstants.PROPERTY_DESCRIPTION,
457    };
458
459    private int[] getSupportedObjectProperties(int format) {
460        switch (format) {
461            case MtpConstants.FORMAT_MP3:
462            case MtpConstants.FORMAT_WAV:
463            case MtpConstants.FORMAT_WMA:
464            case MtpConstants.FORMAT_OGG:
465            case MtpConstants.FORMAT_AAC:
466                return AUDIO_PROPERTIES;
467            case MtpConstants.FORMAT_MPEG:
468            case MtpConstants.FORMAT_3GP_CONTAINER:
469            case MtpConstants.FORMAT_WMV:
470                return VIDEO_PROPERTIES;
471            case MtpConstants.FORMAT_EXIF_JPEG:
472            case MtpConstants.FORMAT_GIF:
473            case MtpConstants.FORMAT_PNG:
474            case MtpConstants.FORMAT_BMP:
475                return IMAGE_PROPERTIES;
476            case 0:
477                return ALL_PROPERTIES;
478            default:
479                return FILE_PROPERTIES;
480        }
481    }
482
483    private int[] getSupportedDeviceProperties() {
484        return new int[] {
485            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
486            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
487            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
488        };
489    }
490
491
492    private MtpPropertyList getObjectPropertyList(long handle, int format, long property,
493                        int groupCode, int depth) {
494        // FIXME - implement group support
495        if (groupCode != 0) {
496            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
497        }
498
499        MtpPropertyGroup propertyGroup;
500        if (property == 0xFFFFFFFFL) {
501             propertyGroup = mPropertyGroupsByFormat.get(format);
502             if (propertyGroup == null) {
503                int[] propertyList = getSupportedObjectProperties(format);
504                propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mVolumeName, propertyList);
505                mPropertyGroupsByFormat.put(new Integer(format), propertyGroup);
506            }
507        } else {
508              propertyGroup = mPropertyGroupsByProperty.get(property);
509             if (propertyGroup == null) {
510                int[] propertyList = new int[] { (int)property };
511                propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mVolumeName, propertyList);
512                mPropertyGroupsByProperty.put(new Integer((int)property), propertyGroup);
513            }
514        }
515
516        return propertyGroup.getPropertyList((int)handle, format, depth, mStorageID);
517    }
518
519    private int renameFile(int handle, String newName) {
520        Cursor c = null;
521
522        // first compute current path
523        String path = null;
524        String[] whereArgs = new String[] {  Integer.toString(handle) };
525        try {
526            c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null);
527            if (c != null && c.moveToNext()) {
528                path = externalToMediaPath(c.getString(1));
529            }
530        } catch (RemoteException e) {
531            Log.e(TAG, "RemoteException in getObjectFilePath", e);
532            return MtpConstants.RESPONSE_GENERAL_ERROR;
533        } finally {
534            if (c != null) {
535                c.close();
536            }
537        }
538        if (path == null) {
539            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
540        }
541
542        // now rename the file.  make sure this succeeds before updating database
543        File oldFile = new File(path);
544        int lastSlash = path.lastIndexOf('/');
545        if (lastSlash <= 1) {
546            return MtpConstants.RESPONSE_GENERAL_ERROR;
547        }
548        String newPath = path.substring(0, lastSlash + 1) + newName;
549        File newFile = new File(newPath);
550        boolean success = oldFile.renameTo(newFile);
551        Log.d(TAG, "renaming "+ path + " to " + newPath + (success ? " succeeded" : " failed"));
552        if (!success) {
553            return MtpConstants.RESPONSE_GENERAL_ERROR;
554        }
555
556        // finally update database
557        ContentValues values = new ContentValues();
558        values.put(Files.FileColumns.DATA, newPath);
559        int updated = 0;
560        try {
561            // note - we are relying on a special case in MediaProvider.update() to update
562            // the paths for all children in the case where this is a directory.
563            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
564        } catch (RemoteException e) {
565            Log.e(TAG, "RemoteException in mMediaProvider.update", e);
566        }
567        if (updated == 0) {
568            Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
569            // this shouldn't happen, but if it does we need to rename the file to its original name
570            newFile.renameTo(oldFile);
571            return MtpConstants.RESPONSE_GENERAL_ERROR;
572        }
573
574        return MtpConstants.RESPONSE_OK;
575    }
576
577    private int setObjectProperty(int handle, int property,
578                            long intValue, String stringValue) {
579        Log.d(TAG, "setObjectProperty: " + property);
580
581        switch (property) {
582            case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
583                return renameFile(handle, stringValue);
584
585            default:
586                return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
587        }
588    }
589
590    private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
591        Log.d(TAG, "getDeviceProperty: " + property);
592
593        switch (property) {
594            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
595            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
596                // writable string properties kept in our device property database
597                Cursor c = null;
598                try {
599                    c = mDevicePropDb.query("properties", DEVICE_PROPERTY_PROJECTION,
600                        DEVICE_PROPERTY_WHERE, new String[] {  Integer.toString(property) },
601                        null, null, null);
602
603                    if (c != null && c.moveToNext()) {
604                        String value = c.getString(1);
605                        int length = value.length();
606                        if (length > 255) {
607                            length = 255;
608                        }
609                        value.getChars(0, length, outStringValue, 0);
610                        outStringValue[length] = 0;
611                    } else {
612                        outStringValue[0] = 0;
613                    }
614                    return MtpConstants.RESPONSE_OK;
615                } finally {
616                    if (c != null) {
617                        c.close();
618                    }
619                }
620
621            case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
622                // use screen size as max image size
623                Display display = ((WindowManager)mContext.getSystemService(
624                        Context.WINDOW_SERVICE)).getDefaultDisplay();
625                int width = display.getWidth();
626                int height = display.getHeight();
627                String imageSize = Integer.toString(width) + "x" +  Integer.toString(height);
628                imageSize.getChars(0, imageSize.length(), outStringValue, 0);
629                outStringValue[imageSize.length()] = 0;
630                return MtpConstants.RESPONSE_OK;
631
632            default:
633                return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
634        }
635    }
636
637    private int setDeviceProperty(int property, long intValue, String stringValue) {
638        Log.d(TAG, "setDeviceProperty: " + property + " : " + stringValue);
639
640        switch (property) {
641            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
642            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
643                // writable string properties kept in our device property database
644                try {
645                    ContentValues values = new ContentValues();
646                    values.put("code", property);
647                    values.put("value", stringValue);
648                    mDevicePropDb.insert("properties", "code", values);
649                    return MtpConstants.RESPONSE_OK;
650                } catch (Exception e) {
651                    return MtpConstants.RESPONSE_GENERAL_ERROR;
652                }
653        }
654
655        return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
656    }
657
658    private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
659                        char[] outName, long[] outSizeModified) {
660        Log.d(TAG, "getObjectInfo: " + handle);
661        Cursor c = null;
662        try {
663            c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION,
664                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
665            if (c != null && c.moveToNext()) {
666                outStorageFormatParent[0] = mStorageID;
667                outStorageFormatParent[1] = c.getInt(2);
668                outStorageFormatParent[2] = c.getInt(3);
669
670                // extract name from path
671                String path = c.getString(1);
672                int lastSlash = path.lastIndexOf('/');
673                int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
674                int end = path.length();
675                if (end - start > 255) {
676                    end = start + 255;
677                }
678                path.getChars(start, end, outName, 0);
679                outName[end - start] = 0;
680
681                outSizeModified[0] = c.getLong(4);
682                outSizeModified[1] = c.getLong(5);
683                return true;
684            }
685        } catch (RemoteException e) {
686            Log.e(TAG, "RemoteException in getObjectInfo", e);
687        } finally {
688            if (c != null) {
689                c.close();
690            }
691        }
692        return false;
693    }
694
695    private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
696        Log.d(TAG, "getObjectFilePath: " + handle);
697        if (handle == 0) {
698            // special case root directory
699            mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
700            outFilePath[mMediaStoragePath.length()] = 0;
701            outFileLengthFormat[0] = 0;
702            outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
703            return MtpConstants.RESPONSE_OK;
704        }
705        Cursor c = null;
706        try {
707            c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION,
708                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
709            if (c != null && c.moveToNext()) {
710                String path = externalToMediaPath(c.getString(1));
711                path.getChars(0, path.length(), outFilePath, 0);
712                outFilePath[path.length()] = 0;
713                outFileLengthFormat[0] = c.getLong(2);
714                outFileLengthFormat[1] = c.getLong(3);
715                return MtpConstants.RESPONSE_OK;
716            } else {
717                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
718            }
719        } catch (RemoteException e) {
720            Log.e(TAG, "RemoteException in getObjectFilePath", e);
721            return MtpConstants.RESPONSE_GENERAL_ERROR;
722        } finally {
723            if (c != null) {
724                c.close();
725            }
726        }
727    }
728
729    private int deleteFile(int handle) {
730        Log.d(TAG, "deleteFile: " + handle);
731        mDatabaseModified = true;
732        String path = null;
733        int format = 0;
734
735        Cursor c = null;
736        try {
737            c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION,
738                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
739            if (c != null && c.moveToNext()) {
740                // don't convert to media path here, since we will be matching
741                // against paths in the database matching /data/media
742                path = c.getString(1);
743                format = c.getInt(3);
744            } else {
745                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
746            }
747
748            if (path == null || format == 0) {
749                return MtpConstants.RESPONSE_GENERAL_ERROR;
750            }
751
752            if (format == MtpConstants.FORMAT_ASSOCIATION) {
753                // recursive case - delete all children first
754                Uri uri = Files.getMtpObjectsUri(mVolumeName);
755                int count = mMediaProvider.delete(uri, "_data LIKE ?",
756                        new String[] { path + "/%"});
757            }
758
759            Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
760            if (mMediaProvider.delete(uri, null, null) > 0) {
761                return MtpConstants.RESPONSE_OK;
762            } else {
763                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
764            }
765        } catch (RemoteException e) {
766            Log.e(TAG, "RemoteException in deleteFile", e);
767            return MtpConstants.RESPONSE_GENERAL_ERROR;
768        } finally {
769            if (c != null) {
770                c.close();
771            }
772        }
773    }
774
775    private int[] getObjectReferences(int handle) {
776        Log.d(TAG, "getObjectReferences for: " + handle);
777        Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
778        Cursor c = null;
779        try {
780            c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null);
781            if (c == null) {
782                return null;
783            }
784            int count = c.getCount();
785            if (count > 0) {
786                int[] result = new int[count];
787                for (int i = 0; i < count; i++) {
788                    c.moveToNext();
789                    result[i] = c.getInt(0);
790                }
791                return result;
792            }
793        } catch (RemoteException e) {
794            Log.e(TAG, "RemoteException in getObjectList", e);
795        } finally {
796            if (c != null) {
797                c.close();
798            }
799        }
800        return null;
801    }
802
803    private int setObjectReferences(int handle, int[] references) {
804        mDatabaseModified = true;
805        Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
806        int count = references.length;
807        ContentValues[] valuesList = new ContentValues[count];
808        for (int i = 0; i < count; i++) {
809            ContentValues values = new ContentValues();
810            values.put(Files.FileColumns._ID, references[i]);
811            valuesList[i] = values;
812        }
813        try {
814            if (mMediaProvider.bulkInsert(uri, valuesList) > 0) {
815                return MtpConstants.RESPONSE_OK;
816            }
817        } catch (RemoteException e) {
818            Log.e(TAG, "RemoteException in setObjectReferences", e);
819        }
820        return MtpConstants.RESPONSE_GENERAL_ERROR;
821    }
822
823    private void sessionStarted() {
824        Log.d(TAG, "sessionStarted");
825        mDatabaseModified = false;
826    }
827
828    private void sessionEnded() {
829        Log.d(TAG, "sessionEnded");
830        if (mDatabaseModified) {
831            Log.d(TAG, "sending ACTION_MTP_SESSION_END");
832            mContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
833            mDatabaseModified = false;
834        }
835    }
836
837    // used by the JNI code
838    private int mNativeContext;
839
840    private native final void native_setup();
841    private native final void native_finalize();
842}
843