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