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