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