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