OSDN Git Service

Validate MTP path
[android-x86/frameworks-base.git] / media / java / android / mtp / MtpDatabase.java
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
17 package android.mtp;
18
19 import android.content.BroadcastReceiver;
20 import android.content.Context;
21 import android.content.ContentValues;
22 import android.content.IContentProvider;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.SharedPreferences;
26 import android.database.Cursor;
27 import android.database.sqlite.SQLiteDatabase;
28 import android.media.MediaScanner;
29 import android.net.Uri;
30 import android.os.BatteryManager;
31 import android.os.BatteryStats;
32 import android.os.RemoteException;
33 import android.provider.MediaStore;
34 import android.provider.MediaStore.Audio;
35 import android.provider.MediaStore.Files;
36 import android.provider.MediaStore.MediaColumns;
37 import android.util.Log;
38 import android.view.Display;
39 import android.view.WindowManager;
40
41 import java.io.File;
42 import java.io.IOException;
43 import java.util.HashMap;
44 import java.util.Locale;
45
46 /**
47  * {@hide}
48  */
49 public 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             if (canonical.startsWith(mMediaStoragePath)) {
310                 return true;
311             }
312         } catch (IOException e) {
313             // ignore
314         }
315         return false;
316     }
317
318     private int beginSendObject(String path, int format, int parent,
319                          int storageId, long size, long modified) {
320         // if the path is outside of the storage root, do not allow access
321         if (!inStorageRoot(path)) {
322             Log.e(TAG, "attempt to put file outside of storage area: " + path);
323             return -1;
324         }
325         // if mSubDirectories is not null, do not allow copying files to any other locations
326         if (!inStorageSubDirectory(path)) return -1;
327
328         // make sure the object does not exist
329         if (path != null) {
330             Cursor c = null;
331             try {
332                 c = mMediaProvider.query(mPackageName, mObjectsUri, ID_PROJECTION, PATH_WHERE,
333                         new String[] { path }, null, null);
334                 if (c != null && c.getCount() > 0) {
335                     Log.w(TAG, "file already exists in beginSendObject: " + path);
336                     return -1;
337                 }
338             } catch (RemoteException e) {
339                 Log.e(TAG, "RemoteException in beginSendObject", e);
340             } finally {
341                 if (c != null) {
342                     c.close();
343                 }
344             }
345         }
346
347         mDatabaseModified = true;
348         ContentValues values = new ContentValues();
349         values.put(Files.FileColumns.DATA, path);
350         values.put(Files.FileColumns.FORMAT, format);
351         values.put(Files.FileColumns.PARENT, parent);
352         values.put(Files.FileColumns.STORAGE_ID, storageId);
353         values.put(Files.FileColumns.SIZE, size);
354         values.put(Files.FileColumns.DATE_MODIFIED, modified);
355
356         try {
357             Uri uri = mMediaProvider.insert(mPackageName, mObjectsUri, values);
358             if (uri != null) {
359                 return Integer.parseInt(uri.getPathSegments().get(2));
360             } else {
361                 return -1;
362             }
363         } catch (RemoteException e) {
364             Log.e(TAG, "RemoteException in beginSendObject", e);
365             return -1;
366         }
367     }
368
369     private void endSendObject(String path, int handle, int format, boolean succeeded) {
370         if (succeeded) {
371             // handle abstract playlists separately
372             // they do not exist in the file system so don't use the media scanner here
373             if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
374                 // extract name from path
375                 String name = path;
376                 int lastSlash = name.lastIndexOf('/');
377                 if (lastSlash >= 0) {
378                     name = name.substring(lastSlash + 1);
379                 }
380                 // strip trailing ".pla" from the name
381                 if (name.endsWith(".pla")) {
382                     name = name.substring(0, name.length() - 4);
383                 }
384
385                 ContentValues values = new ContentValues(1);
386                 values.put(Audio.Playlists.DATA, path);
387                 values.put(Audio.Playlists.NAME, name);
388                 values.put(Files.FileColumns.FORMAT, format);
389                 values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
390                 values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
391                 try {
392                     Uri uri = mMediaProvider.insert(mPackageName,
393                             Audio.Playlists.EXTERNAL_CONTENT_URI, values);
394                 } catch (RemoteException e) {
395                     Log.e(TAG, "RemoteException in endSendObject", e);
396                 }
397             } else {
398                 mMediaScanner.scanMtpFile(path, mVolumeName, handle, format);
399             }
400         } else {
401             deleteFile(handle);
402         }
403     }
404
405     private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException {
406         String where;
407         String[] whereArgs;
408
409         if (storageID == 0xFFFFFFFF) {
410             // query all stores
411             if (format == 0) {
412                 // query all formats
413                 if (parent == 0) {
414                     // query all objects
415                     where = null;
416                     whereArgs = null;
417                 } else {
418                     if (parent == 0xFFFFFFFF) {
419                         // all objects in root of store
420                         parent = 0;
421                     }
422                     where = PARENT_WHERE;
423                     whereArgs = new String[] { Integer.toString(parent) };
424                 }
425             } else {
426                 // query specific format
427                 if (parent == 0) {
428                     // query all objects
429                     where = FORMAT_WHERE;
430                     whereArgs = new String[] { Integer.toString(format) };
431                 } else {
432                     if (parent == 0xFFFFFFFF) {
433                         // all objects in root of store
434                         parent = 0;
435                     }
436                     where = FORMAT_PARENT_WHERE;
437                     whereArgs = new String[] { Integer.toString(format),
438                                                Integer.toString(parent) };
439                 }
440             }
441         } else {
442             // query specific store
443             if (format == 0) {
444                 // query all formats
445                 if (parent == 0) {
446                     // query all objects
447                     where = STORAGE_WHERE;
448                     whereArgs = new String[] { Integer.toString(storageID) };
449                 } else {
450                     if (parent == 0xFFFFFFFF) {
451                         // all objects in root of store
452                         parent = 0;
453                     }
454                     where = STORAGE_PARENT_WHERE;
455                     whereArgs = new String[] { Integer.toString(storageID),
456                                                Integer.toString(parent) };
457                 }
458             } else {
459                 // query specific format
460                 if (parent == 0) {
461                     // query all objects
462                     where = STORAGE_FORMAT_WHERE;
463                     whereArgs = new String[] {  Integer.toString(storageID),
464                                                 Integer.toString(format) };
465                 } else {
466                     if (parent == 0xFFFFFFFF) {
467                         // all objects in root of store
468                         parent = 0;
469                     }
470                     where = STORAGE_FORMAT_PARENT_WHERE;
471                     whereArgs = new String[] { Integer.toString(storageID),
472                                                Integer.toString(format),
473                                                Integer.toString(parent) };
474                 }
475             }
476         }
477
478         // if we are restricting queries to mSubDirectories, we need to add the restriction
479         // onto our "where" arguments
480         if (mSubDirectoriesWhere != null) {
481             if (where == null) {
482                 where = mSubDirectoriesWhere;
483                 whereArgs = mSubDirectoriesWhereArgs;
484             } else {
485                 where = where + " AND " + mSubDirectoriesWhere;
486
487                 // create new array to hold whereArgs and mSubDirectoriesWhereArgs
488                 String[] newWhereArgs =
489                         new String[whereArgs.length + mSubDirectoriesWhereArgs.length];
490                 int i, j;
491                 for (i = 0; i < whereArgs.length; i++) {
492                     newWhereArgs[i] = whereArgs[i];
493                 }
494                 for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) {
495                     newWhereArgs[i] = mSubDirectoriesWhereArgs[j];
496                 }
497                 whereArgs = newWhereArgs;
498             }
499         }
500
501         return mMediaProvider.query(mPackageName, mObjectsUri, ID_PROJECTION, where,
502                 whereArgs, null, null);
503     }
504
505     private int[] getObjectList(int storageID, int format, int parent) {
506         Cursor c = null;
507         try {
508             c = createObjectQuery(storageID, format, parent);
509             if (c == null) {
510                 return null;
511             }
512             int count = c.getCount();
513             if (count > 0) {
514                 int[] result = new int[count];
515                 for (int i = 0; i < count; i++) {
516                     c.moveToNext();
517                     result[i] = c.getInt(0);
518                 }
519                 return result;
520             }
521         } catch (RemoteException e) {
522             Log.e(TAG, "RemoteException in getObjectList", e);
523         } finally {
524             if (c != null) {
525                 c.close();
526             }
527         }
528         return null;
529     }
530
531     private int getNumObjects(int storageID, int format, int parent) {
532         Cursor c = null;
533         try {
534             c = createObjectQuery(storageID, format, parent);
535             if (c != null) {
536                 return c.getCount();
537             }
538         } catch (RemoteException e) {
539             Log.e(TAG, "RemoteException in getNumObjects", e);
540         } finally {
541             if (c != null) {
542                 c.close();
543             }
544         }
545         return -1;
546     }
547
548     private int[] getSupportedPlaybackFormats() {
549         return new int[] {
550             // allow transfering arbitrary files
551             MtpConstants.FORMAT_UNDEFINED,
552
553             MtpConstants.FORMAT_ASSOCIATION,
554             MtpConstants.FORMAT_TEXT,
555             MtpConstants.FORMAT_HTML,
556             MtpConstants.FORMAT_WAV,
557             MtpConstants.FORMAT_MP3,
558             MtpConstants.FORMAT_MPEG,
559             MtpConstants.FORMAT_EXIF_JPEG,
560             MtpConstants.FORMAT_TIFF_EP,
561             MtpConstants.FORMAT_BMP,
562             MtpConstants.FORMAT_GIF,
563             MtpConstants.FORMAT_JFIF,
564             MtpConstants.FORMAT_PNG,
565             MtpConstants.FORMAT_TIFF,
566             MtpConstants.FORMAT_WMA,
567             MtpConstants.FORMAT_OGG,
568             MtpConstants.FORMAT_AAC,
569             MtpConstants.FORMAT_MP4_CONTAINER,
570             MtpConstants.FORMAT_MP2,
571             MtpConstants.FORMAT_3GP_CONTAINER,
572             MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
573             MtpConstants.FORMAT_WPL_PLAYLIST,
574             MtpConstants.FORMAT_M3U_PLAYLIST,
575             MtpConstants.FORMAT_PLS_PLAYLIST,
576             MtpConstants.FORMAT_XML_DOCUMENT,
577             MtpConstants.FORMAT_FLAC,
578         };
579     }
580
581     private int[] getSupportedCaptureFormats() {
582         // no capture formats yet
583         return null;
584     }
585
586     static final int[] FILE_PROPERTIES = {
587             // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
588             // and IMAGE_PROPERTIES below
589             MtpConstants.PROPERTY_STORAGE_ID,
590             MtpConstants.PROPERTY_OBJECT_FORMAT,
591             MtpConstants.PROPERTY_PROTECTION_STATUS,
592             MtpConstants.PROPERTY_OBJECT_SIZE,
593             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
594             MtpConstants.PROPERTY_DATE_MODIFIED,
595             MtpConstants.PROPERTY_PARENT_OBJECT,
596             MtpConstants.PROPERTY_PERSISTENT_UID,
597             MtpConstants.PROPERTY_NAME,
598             MtpConstants.PROPERTY_DATE_ADDED,
599     };
600
601     static final int[] AUDIO_PROPERTIES = {
602             // NOTE must match FILE_PROPERTIES above
603             MtpConstants.PROPERTY_STORAGE_ID,
604             MtpConstants.PROPERTY_OBJECT_FORMAT,
605             MtpConstants.PROPERTY_PROTECTION_STATUS,
606             MtpConstants.PROPERTY_OBJECT_SIZE,
607             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
608             MtpConstants.PROPERTY_DATE_MODIFIED,
609             MtpConstants.PROPERTY_PARENT_OBJECT,
610             MtpConstants.PROPERTY_PERSISTENT_UID,
611             MtpConstants.PROPERTY_NAME,
612             MtpConstants.PROPERTY_DISPLAY_NAME,
613             MtpConstants.PROPERTY_DATE_ADDED,
614
615             // audio specific properties
616             MtpConstants.PROPERTY_ARTIST,
617             MtpConstants.PROPERTY_ALBUM_NAME,
618             MtpConstants.PROPERTY_ALBUM_ARTIST,
619             MtpConstants.PROPERTY_TRACK,
620             MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
621             MtpConstants.PROPERTY_DURATION,
622             MtpConstants.PROPERTY_GENRE,
623             MtpConstants.PROPERTY_COMPOSER,
624             MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
625             MtpConstants.PROPERTY_BITRATE_TYPE,
626             MtpConstants.PROPERTY_AUDIO_BITRATE,
627             MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
628             MtpConstants.PROPERTY_SAMPLE_RATE,
629     };
630
631     static final int[] VIDEO_PROPERTIES = {
632             // NOTE must match FILE_PROPERTIES above
633             MtpConstants.PROPERTY_STORAGE_ID,
634             MtpConstants.PROPERTY_OBJECT_FORMAT,
635             MtpConstants.PROPERTY_PROTECTION_STATUS,
636             MtpConstants.PROPERTY_OBJECT_SIZE,
637             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
638             MtpConstants.PROPERTY_DATE_MODIFIED,
639             MtpConstants.PROPERTY_PARENT_OBJECT,
640             MtpConstants.PROPERTY_PERSISTENT_UID,
641             MtpConstants.PROPERTY_NAME,
642             MtpConstants.PROPERTY_DISPLAY_NAME,
643             MtpConstants.PROPERTY_DATE_ADDED,
644
645             // video specific properties
646             MtpConstants.PROPERTY_ARTIST,
647             MtpConstants.PROPERTY_ALBUM_NAME,
648             MtpConstants.PROPERTY_DURATION,
649             MtpConstants.PROPERTY_DESCRIPTION,
650     };
651
652     static final int[] IMAGE_PROPERTIES = {
653             // NOTE must match FILE_PROPERTIES above
654             MtpConstants.PROPERTY_STORAGE_ID,
655             MtpConstants.PROPERTY_OBJECT_FORMAT,
656             MtpConstants.PROPERTY_PROTECTION_STATUS,
657             MtpConstants.PROPERTY_OBJECT_SIZE,
658             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
659             MtpConstants.PROPERTY_DATE_MODIFIED,
660             MtpConstants.PROPERTY_PARENT_OBJECT,
661             MtpConstants.PROPERTY_PERSISTENT_UID,
662             MtpConstants.PROPERTY_NAME,
663             MtpConstants.PROPERTY_DISPLAY_NAME,
664             MtpConstants.PROPERTY_DATE_ADDED,
665
666             // image specific properties
667             MtpConstants.PROPERTY_DESCRIPTION,
668     };
669
670     static final int[] ALL_PROPERTIES = {
671             // NOTE must match FILE_PROPERTIES above
672             MtpConstants.PROPERTY_STORAGE_ID,
673             MtpConstants.PROPERTY_OBJECT_FORMAT,
674             MtpConstants.PROPERTY_PROTECTION_STATUS,
675             MtpConstants.PROPERTY_OBJECT_SIZE,
676             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
677             MtpConstants.PROPERTY_DATE_MODIFIED,
678             MtpConstants.PROPERTY_PARENT_OBJECT,
679             MtpConstants.PROPERTY_PERSISTENT_UID,
680             MtpConstants.PROPERTY_NAME,
681             MtpConstants.PROPERTY_DISPLAY_NAME,
682             MtpConstants.PROPERTY_DATE_ADDED,
683
684             // image specific properties
685             MtpConstants.PROPERTY_DESCRIPTION,
686
687             // audio specific properties
688             MtpConstants.PROPERTY_ARTIST,
689             MtpConstants.PROPERTY_ALBUM_NAME,
690             MtpConstants.PROPERTY_ALBUM_ARTIST,
691             MtpConstants.PROPERTY_TRACK,
692             MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
693             MtpConstants.PROPERTY_DURATION,
694             MtpConstants.PROPERTY_GENRE,
695             MtpConstants.PROPERTY_COMPOSER,
696
697             // video specific properties
698             MtpConstants.PROPERTY_ARTIST,
699             MtpConstants.PROPERTY_ALBUM_NAME,
700             MtpConstants.PROPERTY_DURATION,
701             MtpConstants.PROPERTY_DESCRIPTION,
702
703             // image specific properties
704             MtpConstants.PROPERTY_DESCRIPTION,
705     };
706
707     private int[] getSupportedObjectProperties(int format) {
708         switch (format) {
709             case MtpConstants.FORMAT_MP3:
710             case MtpConstants.FORMAT_WAV:
711             case MtpConstants.FORMAT_WMA:
712             case MtpConstants.FORMAT_OGG:
713             case MtpConstants.FORMAT_AAC:
714                 return AUDIO_PROPERTIES;
715             case MtpConstants.FORMAT_MPEG:
716             case MtpConstants.FORMAT_3GP_CONTAINER:
717             case MtpConstants.FORMAT_WMV:
718                 return VIDEO_PROPERTIES;
719             case MtpConstants.FORMAT_EXIF_JPEG:
720             case MtpConstants.FORMAT_GIF:
721             case MtpConstants.FORMAT_PNG:
722             case MtpConstants.FORMAT_BMP:
723                 return IMAGE_PROPERTIES;
724             case 0:
725                 return ALL_PROPERTIES;
726             default:
727                 return FILE_PROPERTIES;
728         }
729     }
730
731     private int[] getSupportedDeviceProperties() {
732         return new int[] {
733             MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
734             MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
735             MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
736             MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
737         };
738     }
739
740
741     private MtpPropertyList getObjectPropertyList(long handle, int format, long property,
742                         int groupCode, int depth) {
743         // FIXME - implement group support
744         if (groupCode != 0) {
745             return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
746         }
747
748         MtpPropertyGroup propertyGroup;
749         if (property == 0xFFFFFFFFL) {
750              propertyGroup = mPropertyGroupsByFormat.get(format);
751              if (propertyGroup == null) {
752                 int[] propertyList = getSupportedObjectProperties(format);
753                 propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mPackageName,
754                         mVolumeName, propertyList);
755                 mPropertyGroupsByFormat.put(new Integer(format), propertyGroup);
756             }
757         } else {
758               propertyGroup = mPropertyGroupsByProperty.get(property);
759              if (propertyGroup == null) {
760                 int[] propertyList = new int[] { (int)property };
761                 propertyGroup = new MtpPropertyGroup(this, mMediaProvider, mPackageName,
762                         mVolumeName, propertyList);
763                 mPropertyGroupsByProperty.put(new Integer((int)property), propertyGroup);
764             }
765         }
766
767         return propertyGroup.getPropertyList((int)handle, format, depth);
768     }
769
770     private int renameFile(int handle, String newName) {
771         Cursor c = null;
772
773         // first compute current path
774         String path = null;
775         String[] whereArgs = new String[] {  Integer.toString(handle) };
776         try {
777             c = mMediaProvider.query(mPackageName, mObjectsUri, PATH_PROJECTION, ID_WHERE,
778                     whereArgs, null, null);
779             if (c != null && c.moveToNext()) {
780                 path = c.getString(1);
781             }
782         } catch (RemoteException e) {
783             Log.e(TAG, "RemoteException in getObjectFilePath", e);
784             return MtpConstants.RESPONSE_GENERAL_ERROR;
785         } finally {
786             if (c != null) {
787                 c.close();
788             }
789         }
790         if (path == null) {
791             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
792         }
793
794         // do not allow renaming any of the special subdirectories
795         if (isStorageSubDirectory(path)) {
796             return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
797         }
798
799         // now rename the file.  make sure this succeeds before updating database
800         File oldFile = new File(path);
801         int lastSlash = path.lastIndexOf('/');
802         if (lastSlash <= 1) {
803             return MtpConstants.RESPONSE_GENERAL_ERROR;
804         }
805         String newPath = path.substring(0, lastSlash + 1) + newName;
806         File newFile = new File(newPath);
807         boolean success = oldFile.renameTo(newFile);
808         if (!success) {
809             Log.w(TAG, "renaming "+ path + " to " + newPath + " failed");
810             return MtpConstants.RESPONSE_GENERAL_ERROR;
811         }
812
813         // finally update database
814         ContentValues values = new ContentValues();
815         values.put(Files.FileColumns.DATA, newPath);
816         int updated = 0;
817         try {
818             // note - we are relying on a special case in MediaProvider.update() to update
819             // the paths for all children in the case where this is a directory.
820             updated = mMediaProvider.update(mPackageName, mObjectsUri, values, ID_WHERE, whereArgs);
821         } catch (RemoteException e) {
822             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
823         }
824         if (updated == 0) {
825             Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
826             // this shouldn't happen, but if it does we need to rename the file to its original name
827             newFile.renameTo(oldFile);
828             return MtpConstants.RESPONSE_GENERAL_ERROR;
829         }
830
831         // check if nomedia status changed
832         if (newFile.isDirectory()) {
833             // for directories, check if renamed from something hidden to something non-hidden
834             if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) {
835                 // directory was unhidden
836                 try {
837                     mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, newPath, null);
838                 } catch (RemoteException e) {
839                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
840                 }
841             }
842         } else {
843             // for files, check if renamed from .nomedia to something else
844             if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia")
845                     && !newPath.toLowerCase(Locale.US).equals(".nomedia")) {
846                 try {
847                     mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, oldFile.getParent(), null);
848                 } catch (RemoteException e) {
849                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
850                 }
851             }
852         }
853
854         return MtpConstants.RESPONSE_OK;
855     }
856
857     private int setObjectProperty(int handle, int property,
858                             long intValue, String stringValue) {
859         switch (property) {
860             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
861                 return renameFile(handle, stringValue);
862
863             default:
864                 return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
865         }
866     }
867
868     private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
869         switch (property) {
870             case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
871             case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
872                 // writable string properties kept in shared preferences
873                 String value = mDeviceProperties.getString(Integer.toString(property), "");
874                 int length = value.length();
875                 if (length > 255) {
876                     length = 255;
877                 }
878                 value.getChars(0, length, outStringValue, 0);
879                 outStringValue[length] = 0;
880                 return MtpConstants.RESPONSE_OK;
881
882             case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
883                 // use screen size as max image size
884                 Display display = ((WindowManager)mContext.getSystemService(
885                         Context.WINDOW_SERVICE)).getDefaultDisplay();
886                 int width = display.getMaximumSizeDimension();
887                 int height = display.getMaximumSizeDimension();
888                 String imageSize = Integer.toString(width) + "x" +  Integer.toString(height);
889                 imageSize.getChars(0, imageSize.length(), outStringValue, 0);
890                 outStringValue[imageSize.length()] = 0;
891                 return MtpConstants.RESPONSE_OK;
892
893             // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code
894
895             default:
896                 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
897         }
898     }
899
900     private int setDeviceProperty(int property, long intValue, String stringValue) {
901         switch (property) {
902             case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
903             case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
904                 // writable string properties kept in shared prefs
905                 SharedPreferences.Editor e = mDeviceProperties.edit();
906                 e.putString(Integer.toString(property), stringValue);
907                 return (e.commit() ? MtpConstants.RESPONSE_OK
908                         : MtpConstants.RESPONSE_GENERAL_ERROR);
909         }
910
911         return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
912     }
913
914     private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
915                         char[] outName, long[] outCreatedModified) {
916         Cursor c = null;
917         try {
918             c = mMediaProvider.query(mPackageName, mObjectsUri, OBJECT_INFO_PROJECTION,
919                             ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
920             if (c != null && c.moveToNext()) {
921                 outStorageFormatParent[0] = c.getInt(1);
922                 outStorageFormatParent[1] = c.getInt(2);
923                 outStorageFormatParent[2] = c.getInt(3);
924
925                 // extract name from path
926                 String path = c.getString(4);
927                 int lastSlash = path.lastIndexOf('/');
928                 int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
929                 int end = path.length();
930                 if (end - start > 255) {
931                     end = start + 255;
932                 }
933                 path.getChars(start, end, outName, 0);
934                 outName[end - start] = 0;
935
936                 outCreatedModified[0] = c.getLong(5);
937                 outCreatedModified[1] = c.getLong(6);
938                 // use modification date as creation date if date added is not set
939                 if (outCreatedModified[0] == 0) {
940                     outCreatedModified[0] = outCreatedModified[1];
941                 }
942                 return true;
943             }
944         } catch (RemoteException e) {
945             Log.e(TAG, "RemoteException in getObjectInfo", e);
946         } finally {
947             if (c != null) {
948                 c.close();
949             }
950         }
951         return false;
952     }
953
954     private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
955         if (handle == 0) {
956             // special case root directory
957             mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
958             outFilePath[mMediaStoragePath.length()] = 0;
959             outFileLengthFormat[0] = 0;
960             outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
961             return MtpConstants.RESPONSE_OK;
962         }
963         Cursor c = null;
964         try {
965             c = mMediaProvider.query(mPackageName, mObjectsUri, PATH_FORMAT_PROJECTION,
966                             ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
967             if (c != null && c.moveToNext()) {
968                 String path = c.getString(1);
969                 path.getChars(0, path.length(), outFilePath, 0);
970                 outFilePath[path.length()] = 0;
971                 // File transfers from device to host will likely fail if the size is incorrect.
972                 // So to be safe, use the actual file size here.
973                 outFileLengthFormat[0] = new File(path).length();
974                 outFileLengthFormat[1] = c.getLong(2);
975                 return MtpConstants.RESPONSE_OK;
976             } else {
977                 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
978             }
979         } catch (RemoteException e) {
980             Log.e(TAG, "RemoteException in getObjectFilePath", e);
981             return MtpConstants.RESPONSE_GENERAL_ERROR;
982         } finally {
983             if (c != null) {
984                 c.close();
985             }
986         }
987     }
988
989     private int deleteFile(int handle) {
990         mDatabaseModified = true;
991         String path = null;
992         int format = 0;
993
994         Cursor c = null;
995         try {
996             c = mMediaProvider.query(mPackageName, mObjectsUri, PATH_FORMAT_PROJECTION,
997                             ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
998             if (c != null && c.moveToNext()) {
999                 // don't convert to media path here, since we will be matching
1000                 // against paths in the database matching /data/media
1001                 path = c.getString(1);
1002                 format = c.getInt(2);
1003             } else {
1004                 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
1005             }
1006
1007             if (path == null || format == 0) {
1008                 return MtpConstants.RESPONSE_GENERAL_ERROR;
1009             }
1010
1011             // do not allow deleting any of the special subdirectories
1012             if (isStorageSubDirectory(path)) {
1013                 return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
1014             }
1015
1016             if (format == MtpConstants.FORMAT_ASSOCIATION) {
1017                 // recursive case - delete all children first
1018                 Uri uri = Files.getMtpObjectsUri(mVolumeName);
1019                 int count = mMediaProvider.delete(mPackageName, uri,
1020                     // the 'like' makes it use the index, the 'lower()' makes it correct
1021                     // when the path contains sqlite wildcard characters
1022                     "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
1023                     new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"});
1024             }
1025
1026             Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
1027             if (mMediaProvider.delete(mPackageName, uri, null, null) > 0) {
1028                 if (format != MtpConstants.FORMAT_ASSOCIATION
1029                         && path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
1030                     try {
1031                         String parentPath = path.substring(0, path.lastIndexOf("/"));
1032                         mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, parentPath, null);
1033                     } catch (RemoteException e) {
1034                         Log.e(TAG, "failed to unhide/rescan for " + path);
1035                     }
1036                 }
1037                 return MtpConstants.RESPONSE_OK;
1038             } else {
1039                 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
1040             }
1041         } catch (RemoteException e) {
1042             Log.e(TAG, "RemoteException in deleteFile", e);
1043             return MtpConstants.RESPONSE_GENERAL_ERROR;
1044         } finally {
1045             if (c != null) {
1046                 c.close();
1047             }
1048         }
1049     }
1050
1051     private int[] getObjectReferences(int handle) {
1052         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
1053         Cursor c = null;
1054         try {
1055             c = mMediaProvider.query(mPackageName, uri, ID_PROJECTION, null, null, null, null);
1056             if (c == null) {
1057                 return null;
1058             }
1059             int count = c.getCount();
1060             if (count > 0) {
1061                 int[] result = new int[count];
1062                 for (int i = 0; i < count; i++) {
1063                     c.moveToNext();
1064                     result[i] = c.getInt(0);
1065                 }
1066                 return result;
1067             }
1068         } catch (RemoteException e) {
1069             Log.e(TAG, "RemoteException in getObjectList", e);
1070         } finally {
1071             if (c != null) {
1072                 c.close();
1073             }
1074         }
1075         return null;
1076     }
1077
1078     private int setObjectReferences(int handle, int[] references) {
1079         mDatabaseModified = true;
1080         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
1081         int count = references.length;
1082         ContentValues[] valuesList = new ContentValues[count];
1083         for (int i = 0; i < count; i++) {
1084             ContentValues values = new ContentValues();
1085             values.put(Files.FileColumns._ID, references[i]);
1086             valuesList[i] = values;
1087         }
1088         try {
1089             if (mMediaProvider.bulkInsert(mPackageName, uri, valuesList) > 0) {
1090                 return MtpConstants.RESPONSE_OK;
1091             }
1092         } catch (RemoteException e) {
1093             Log.e(TAG, "RemoteException in setObjectReferences", e);
1094         }
1095         return MtpConstants.RESPONSE_GENERAL_ERROR;
1096     }
1097
1098     private void sessionStarted() {
1099         mDatabaseModified = false;
1100     }
1101
1102     private void sessionEnded() {
1103         if (mDatabaseModified) {
1104             mContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
1105             mDatabaseModified = false;
1106         }
1107     }
1108
1109     // used by the JNI code
1110     private long mNativeContext;
1111
1112     private native final void native_setup();
1113     private native final void native_finalize();
1114 }