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