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