MtpDatabase.java revision 8ed67acf38ccdfdb89553ed58620a135f2100e10
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        Log.d(TAG, "getObjectList parent: " + parent);
226        Cursor c = null;
227        try {
228            if (format != 0) {
229                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
230                            PARENT_FORMAT_WHERE,
231                            new String[] { Integer.toString(parent), Integer.toString(format) },
232                             null);
233            } else {
234                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
235                            PARENT_WHERE, new String[] { Integer.toString(parent) }, null);
236            }
237            if (c == null) {
238                Log.d(TAG, "null cursor");
239                return null;
240            }
241            int count = c.getCount();
242            if (count > 0) {
243                int[] result = new int[count];
244                for (int i = 0; i < count; i++) {
245                    c.moveToNext();
246                    result[i] = c.getInt(0);
247                }
248                Log.d(TAG, "returning " + result);
249                return result;
250            }
251        } catch (RemoteException e) {
252            Log.e(TAG, "RemoteException in getObjectList", e);
253        } finally {
254            if (c != null) {
255                c.close();
256            }
257        }
258        return null;
259    }
260
261    private int getNumObjects(int storageID, int format, int parent) {
262        // we can ignore storageID until we support multiple storages
263        Log.d(TAG, "getObjectList parent: " + parent);
264        Cursor c = null;
265        try {
266            if (format != 0) {
267                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
268                            PARENT_FORMAT_WHERE,
269                            new String[] { Integer.toString(parent), Integer.toString(format) },
270                             null);
271            } else {
272                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION,
273                            PARENT_WHERE, new String[] { Integer.toString(parent) }, null);
274            }
275            if (c != null) {
276                return c.getCount();
277            }
278        } catch (RemoteException e) {
279            Log.e(TAG, "RemoteException in getNumObjects", e);
280        } finally {
281            if (c != null) {
282                c.close();
283            }
284        }
285        return -1;
286    }
287
288    private int[] getSupportedPlaybackFormats() {
289        return new int[] {
290            // allow transfering arbitrary files
291            MtpConstants.FORMAT_UNDEFINED,
292
293            MtpConstants.FORMAT_ASSOCIATION,
294            MtpConstants.FORMAT_TEXT,
295            MtpConstants.FORMAT_HTML,
296            MtpConstants.FORMAT_WAV,
297            MtpConstants.FORMAT_MP3,
298            MtpConstants.FORMAT_MPEG,
299            MtpConstants.FORMAT_EXIF_JPEG,
300            MtpConstants.FORMAT_TIFF_EP,
301            MtpConstants.FORMAT_GIF,
302            MtpConstants.FORMAT_JFIF,
303            MtpConstants.FORMAT_PNG,
304            MtpConstants.FORMAT_TIFF,
305            MtpConstants.FORMAT_WMA,
306            MtpConstants.FORMAT_OGG,
307            MtpConstants.FORMAT_AAC,
308            MtpConstants.FORMAT_MP4_CONTAINER,
309            MtpConstants.FORMAT_MP2,
310            MtpConstants.FORMAT_3GP_CONTAINER,
311            MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
312            MtpConstants.FORMAT_WPL_PLAYLIST,
313            MtpConstants.FORMAT_M3U_PLAYLIST,
314            MtpConstants.FORMAT_PLS_PLAYLIST,
315            MtpConstants.FORMAT_XML_DOCUMENT,
316        };
317    }
318
319    private int[] getSupportedCaptureFormats() {
320        // no capture formats yet
321        return null;
322    }
323
324    static final int[] FILE_PROPERTIES = {
325            // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
326            // and IMAGE_PROPERTIES below
327            MtpConstants.PROPERTY_STORAGE_ID,
328            MtpConstants.PROPERTY_OBJECT_FORMAT,
329            MtpConstants.PROPERTY_PROTECTION_STATUS,
330            MtpConstants.PROPERTY_OBJECT_SIZE,
331            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
332            MtpConstants.PROPERTY_DATE_MODIFIED,
333            MtpConstants.PROPERTY_PARENT_OBJECT,
334            MtpConstants.PROPERTY_PERSISTENT_UID,
335            MtpConstants.PROPERTY_NAME,
336            MtpConstants.PROPERTY_DATE_ADDED,
337    };
338
339    static final int[] AUDIO_PROPERTIES = {
340            // NOTE must match FILE_PROPERTIES above
341            MtpConstants.PROPERTY_STORAGE_ID,
342            MtpConstants.PROPERTY_OBJECT_FORMAT,
343            MtpConstants.PROPERTY_PROTECTION_STATUS,
344            MtpConstants.PROPERTY_OBJECT_SIZE,
345            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
346            MtpConstants.PROPERTY_DATE_MODIFIED,
347            MtpConstants.PROPERTY_PARENT_OBJECT,
348            MtpConstants.PROPERTY_PERSISTENT_UID,
349            MtpConstants.PROPERTY_NAME,
350            MtpConstants.PROPERTY_DISPLAY_NAME,
351            MtpConstants.PROPERTY_DATE_ADDED,
352
353            // audio specific properties
354            MtpConstants.PROPERTY_ARTIST,
355            MtpConstants.PROPERTY_ALBUM_NAME,
356            MtpConstants.PROPERTY_ALBUM_ARTIST,
357            MtpConstants.PROPERTY_TRACK,
358            MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
359            MtpConstants.PROPERTY_DURATION,
360            MtpConstants.PROPERTY_GENRE,
361            MtpConstants.PROPERTY_COMPOSER,
362    };
363
364    static final int[] VIDEO_PROPERTIES = {
365            // NOTE must match FILE_PROPERTIES above
366            MtpConstants.PROPERTY_STORAGE_ID,
367            MtpConstants.PROPERTY_OBJECT_FORMAT,
368            MtpConstants.PROPERTY_PROTECTION_STATUS,
369            MtpConstants.PROPERTY_OBJECT_SIZE,
370            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
371            MtpConstants.PROPERTY_DATE_MODIFIED,
372            MtpConstants.PROPERTY_PARENT_OBJECT,
373            MtpConstants.PROPERTY_PERSISTENT_UID,
374            MtpConstants.PROPERTY_NAME,
375            MtpConstants.PROPERTY_DISPLAY_NAME,
376            MtpConstants.PROPERTY_DATE_ADDED,
377
378            // video specific properties
379            MtpConstants.PROPERTY_ARTIST,
380            MtpConstants.PROPERTY_ALBUM_NAME,
381            MtpConstants.PROPERTY_DURATION,
382            MtpConstants.PROPERTY_DESCRIPTION,
383    };
384
385    static final int[] IMAGE_PROPERTIES = {
386            // NOTE must match FILE_PROPERTIES above
387            MtpConstants.PROPERTY_STORAGE_ID,
388            MtpConstants.PROPERTY_OBJECT_FORMAT,
389            MtpConstants.PROPERTY_PROTECTION_STATUS,
390            MtpConstants.PROPERTY_OBJECT_SIZE,
391            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
392            MtpConstants.PROPERTY_DATE_MODIFIED,
393            MtpConstants.PROPERTY_PARENT_OBJECT,
394            MtpConstants.PROPERTY_PERSISTENT_UID,
395            MtpConstants.PROPERTY_NAME,
396            MtpConstants.PROPERTY_DISPLAY_NAME,
397            MtpConstants.PROPERTY_DATE_ADDED,
398
399            // image specific properties
400            MtpConstants.PROPERTY_DESCRIPTION,
401    };
402
403    static final int[] ALL_PROPERTIES = {
404            // NOTE must match FILE_PROPERTIES above
405            MtpConstants.PROPERTY_STORAGE_ID,
406            MtpConstants.PROPERTY_OBJECT_FORMAT,
407            MtpConstants.PROPERTY_PROTECTION_STATUS,
408            MtpConstants.PROPERTY_OBJECT_SIZE,
409            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
410            MtpConstants.PROPERTY_DATE_MODIFIED,
411            MtpConstants.PROPERTY_PARENT_OBJECT,
412            MtpConstants.PROPERTY_PERSISTENT_UID,
413            MtpConstants.PROPERTY_NAME,
414            MtpConstants.PROPERTY_DISPLAY_NAME,
415            MtpConstants.PROPERTY_DATE_ADDED,
416
417            // image specific properties
418            MtpConstants.PROPERTY_DESCRIPTION,
419
420            // audio specific properties
421            MtpConstants.PROPERTY_ARTIST,
422            MtpConstants.PROPERTY_ALBUM_NAME,
423            MtpConstants.PROPERTY_ALBUM_ARTIST,
424            MtpConstants.PROPERTY_TRACK,
425            MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
426            MtpConstants.PROPERTY_DURATION,
427            MtpConstants.PROPERTY_GENRE,
428            MtpConstants.PROPERTY_COMPOSER,
429
430            // video specific properties
431            MtpConstants.PROPERTY_ARTIST,
432            MtpConstants.PROPERTY_ALBUM_NAME,
433            MtpConstants.PROPERTY_DURATION,
434            MtpConstants.PROPERTY_DESCRIPTION,
435
436            // image specific properties
437            MtpConstants.PROPERTY_DESCRIPTION,
438    };
439
440    private int[] getSupportedObjectProperties(int format) {
441        switch (format) {
442            case MtpConstants.FORMAT_MP3:
443            case MtpConstants.FORMAT_WAV:
444            case MtpConstants.FORMAT_WMA:
445            case MtpConstants.FORMAT_OGG:
446            case MtpConstants.FORMAT_AAC:
447                return AUDIO_PROPERTIES;
448            case MtpConstants.FORMAT_MPEG:
449            case MtpConstants.FORMAT_3GP_CONTAINER:
450            case MtpConstants.FORMAT_WMV:
451                return VIDEO_PROPERTIES;
452            case MtpConstants.FORMAT_EXIF_JPEG:
453            case MtpConstants.FORMAT_GIF:
454            case MtpConstants.FORMAT_PNG:
455            case MtpConstants.FORMAT_BMP:
456                return IMAGE_PROPERTIES;
457            case 0:
458                return ALL_PROPERTIES;
459            default:
460                return FILE_PROPERTIES;
461        }
462    }
463
464    private int[] getSupportedDeviceProperties() {
465        return new int[] {
466            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
467            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
468            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
469        };
470    }
471
472
473    private MtpPropertyList getObjectPropertyList(long handle, int format, long property,
474                        int groupCode, int depth) {
475        // FIXME - implement group support
476        if (groupCode != 0) {
477            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
478        }
479
480        MtpPropertyGroup propertyGroup;
481        if (property == 0xFFFFFFFFL) {
482             propertyGroup = mPropertyGroupsByFormat.get(format);
483             if (propertyGroup == null) {
484                int[] propertyList = getSupportedObjectProperties(format);
485                propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mVolumeName, propertyList);
486                mPropertyGroupsByFormat.put(new Integer(format), propertyGroup);
487            }
488        } else {
489              propertyGroup = mPropertyGroupsByProperty.get(property);
490             if (propertyGroup == null) {
491                int[] propertyList = new int[] { (int)property };
492                propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mVolumeName, propertyList);
493                mPropertyGroupsByProperty.put(new Integer((int)property), propertyGroup);
494            }
495        }
496
497        return propertyGroup.getPropertyList((int)handle, format, depth, mStorageID);
498    }
499
500    private int renameFile(int handle, String newName) {
501        Cursor c = null;
502
503        // first compute current path
504        String path = null;
505        String[] whereArgs = new String[] {  Integer.toString(handle) };
506        try {
507            c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null);
508            if (c != null && c.moveToNext()) {
509                path = c.getString(1);
510            }
511        } catch (RemoteException e) {
512            Log.e(TAG, "RemoteException in getObjectFilePath", e);
513            return MtpConstants.RESPONSE_GENERAL_ERROR;
514        } finally {
515            if (c != null) {
516                c.close();
517            }
518        }
519        if (path == null) {
520            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
521        }
522
523        // now rename the file.  make sure this succeeds before updating database
524        File oldFile = new File(path);
525        int lastSlash = path.lastIndexOf('/');
526        if (lastSlash <= 1) {
527            return MtpConstants.RESPONSE_GENERAL_ERROR;
528        }
529        String newPath = path.substring(0, lastSlash + 1) + newName;
530        File newFile = new File(newPath);
531        boolean success = oldFile.renameTo(newFile);
532        Log.d(TAG, "renaming "+ path + " to " + newPath + (success ? " succeeded" : " failed"));
533        if (!success) {
534            return MtpConstants.RESPONSE_GENERAL_ERROR;
535        }
536
537        // finally update database
538        ContentValues values = new ContentValues();
539        values.put(Files.FileColumns.DATA, newPath);
540        int updated = 0;
541        try {
542            // note - we are relying on a special case in MediaProvider.update() to update
543            // the paths for all children in the case where this is a directory.
544            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
545        } catch (RemoteException e) {
546            Log.e(TAG, "RemoteException in mMediaProvider.update", e);
547        }
548        if (updated == 0) {
549            Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
550            // this shouldn't happen, but if it does we need to rename the file to its original name
551            newFile.renameTo(oldFile);
552            return MtpConstants.RESPONSE_GENERAL_ERROR;
553        }
554
555        return MtpConstants.RESPONSE_OK;
556    }
557
558    private int setObjectProperty(int handle, int property,
559                            long intValue, String stringValue) {
560        Log.d(TAG, "setObjectProperty: " + property);
561
562        switch (property) {
563            case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
564                return renameFile(handle, stringValue);
565
566            default:
567                return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
568        }
569    }
570
571    private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
572        Log.d(TAG, "getDeviceProperty: " + property);
573
574        switch (property) {
575            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
576            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
577                // writable string properties kept in our device property database
578                Cursor c = null;
579                try {
580                    c = mDevicePropDb.query("properties", DEVICE_PROPERTY_PROJECTION,
581                        DEVICE_PROPERTY_WHERE, new String[] {  Integer.toString(property) },
582                        null, null, null);
583
584                    if (c != null && c.moveToNext()) {
585                        String value = c.getString(1);
586                        int length = value.length();
587                        if (length > 255) {
588                            length = 255;
589                        }
590                        value.getChars(0, length, outStringValue, 0);
591                        outStringValue[length] = 0;
592                    } else {
593                        outStringValue[0] = 0;
594                    }
595                    return MtpConstants.RESPONSE_OK;
596                } finally {
597                    if (c != null) {
598                        c.close();
599                    }
600                }
601
602            case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
603                // use screen size as max image size
604                Display display = ((WindowManager)mContext.getSystemService(
605                        Context.WINDOW_SERVICE)).getDefaultDisplay();
606                int width = display.getWidth();
607                int height = display.getHeight();
608                String imageSize = Integer.toString(width) + "x" +  Integer.toString(height);
609                imageSize.getChars(0, imageSize.length(), outStringValue, 0);
610                outStringValue[imageSize.length()] = 0;
611                return MtpConstants.RESPONSE_OK;
612
613            default:
614                return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
615        }
616    }
617
618    private int setDeviceProperty(int property, long intValue, String stringValue) {
619        Log.d(TAG, "setDeviceProperty: " + property + " : " + stringValue);
620
621        switch (property) {
622            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
623            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
624                // writable string properties kept in our device property database
625                try {
626                    ContentValues values = new ContentValues();
627                    values.put("code", property);
628                    values.put("value", stringValue);
629                    mDevicePropDb.insert("properties", "code", values);
630                    return MtpConstants.RESPONSE_OK;
631                } catch (Exception e) {
632                    return MtpConstants.RESPONSE_GENERAL_ERROR;
633                }
634        }
635
636        return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
637    }
638
639    private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
640                        char[] outName, long[] outSizeModified) {
641        Log.d(TAG, "getObjectInfo: " + handle);
642        Cursor c = null;
643        try {
644            c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION,
645                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
646            if (c != null && c.moveToNext()) {
647                outStorageFormatParent[0] = mStorageID;
648                outStorageFormatParent[1] = c.getInt(2);
649                outStorageFormatParent[2] = c.getInt(3);
650
651                // extract name from path
652                String path = c.getString(1);
653                int lastSlash = path.lastIndexOf('/');
654                int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
655                int end = path.length();
656                if (end - start > 255) {
657                    end = start + 255;
658                }
659                path.getChars(start, end, outName, 0);
660                outName[end - start] = 0;
661
662                outSizeModified[0] = c.getLong(4);
663                outSizeModified[1] = c.getLong(5);
664                return true;
665            }
666        } catch (RemoteException e) {
667            Log.e(TAG, "RemoteException in getObjectInfo", e);
668        } finally {
669            if (c != null) {
670                c.close();
671            }
672        }
673        return false;
674    }
675
676    private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
677        Log.d(TAG, "getObjectFilePath: " + handle);
678        if (handle == 0) {
679            // special case root directory
680            mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
681            outFilePath[mMediaStoragePath.length()] = 0;
682            outFileLengthFormat[0] = 0;
683            outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
684            return MtpConstants.RESPONSE_OK;
685        }
686        Cursor c = null;
687        try {
688            c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION,
689                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
690            if (c != null && c.moveToNext()) {
691                String path = c.getString(1);
692                path.getChars(0, path.length(), outFilePath, 0);
693                outFilePath[path.length()] = 0;
694                outFileLengthFormat[0] = c.getLong(2);
695                outFileLengthFormat[1] = c.getLong(3);
696                return MtpConstants.RESPONSE_OK;
697            } else {
698                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
699            }
700        } catch (RemoteException e) {
701            Log.e(TAG, "RemoteException in getObjectFilePath", e);
702            return MtpConstants.RESPONSE_GENERAL_ERROR;
703        } finally {
704            if (c != null) {
705                c.close();
706            }
707        }
708    }
709
710    private int deleteFile(int handle) {
711        Log.d(TAG, "deleteFile: " + handle);
712        mDatabaseModified = true;
713        String path = null;
714        int format = 0;
715
716        Cursor c = null;
717        try {
718            c = mMediaProvider.query(mObjectsUri, PATH_SIZE_FORMAT_PROJECTION,
719                            ID_WHERE, new String[] {  Integer.toString(handle) }, null);
720            if (c != null && c.moveToNext()) {
721                // don't convert to media path here, since we will be matching
722                // against paths in the database matching /data/media
723                path = c.getString(1);
724                format = c.getInt(3);
725            } else {
726                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
727            }
728
729            if (path == null || format == 0) {
730                return MtpConstants.RESPONSE_GENERAL_ERROR;
731            }
732
733            if (format == MtpConstants.FORMAT_ASSOCIATION) {
734                // recursive case - delete all children first
735                Uri uri = Files.getMtpObjectsUri(mVolumeName);
736                int count = mMediaProvider.delete(uri, "_data LIKE ?",
737                        new String[] { path + "/%"});
738            }
739
740            Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
741            if (mMediaProvider.delete(uri, null, null) > 0) {
742                return MtpConstants.RESPONSE_OK;
743            } else {
744                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
745            }
746        } catch (RemoteException e) {
747            Log.e(TAG, "RemoteException in deleteFile", e);
748            return MtpConstants.RESPONSE_GENERAL_ERROR;
749        } finally {
750            if (c != null) {
751                c.close();
752            }
753        }
754    }
755
756    private int[] getObjectReferences(int handle) {
757        Log.d(TAG, "getObjectReferences for: " + handle);
758        Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
759        Cursor c = null;
760        try {
761            c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null);
762            if (c == null) {
763                return null;
764            }
765            int count = c.getCount();
766            if (count > 0) {
767                int[] result = new int[count];
768                for (int i = 0; i < count; i++) {
769                    c.moveToNext();
770                    result[i] = c.getInt(0);
771                }
772                return result;
773            }
774        } catch (RemoteException e) {
775            Log.e(TAG, "RemoteException in getObjectList", e);
776        } finally {
777            if (c != null) {
778                c.close();
779            }
780        }
781        return null;
782    }
783
784    private int setObjectReferences(int handle, int[] references) {
785        mDatabaseModified = true;
786        Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
787        int count = references.length;
788        ContentValues[] valuesList = new ContentValues[count];
789        for (int i = 0; i < count; i++) {
790            ContentValues values = new ContentValues();
791            values.put(Files.FileColumns._ID, references[i]);
792            valuesList[i] = values;
793        }
794        try {
795            if (mMediaProvider.bulkInsert(uri, valuesList) > 0) {
796                return MtpConstants.RESPONSE_OK;
797            }
798        } catch (RemoteException e) {
799            Log.e(TAG, "RemoteException in setObjectReferences", e);
800        }
801        return MtpConstants.RESPONSE_GENERAL_ERROR;
802    }
803
804    private void sessionStarted() {
805        Log.d(TAG, "sessionStarted");
806        mDatabaseModified = false;
807    }
808
809    private void sessionEnded() {
810        Log.d(TAG, "sessionEnded");
811        if (mDatabaseModified) {
812            Log.d(TAG, "sending ACTION_MTP_SESSION_END");
813            mContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
814            mDatabaseModified = false;
815        }
816    }
817
818    // used by the JNI code
819    private int mNativeContext;
820
821    private native final void native_setup();
822    private native final void native_finalize();
823}
824