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