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