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