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