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