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