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