• 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.os.storage.StorageVolume;
34 import android.provider.MediaStore;
35 import android.provider.MediaStore.Audio;
36 import android.provider.MediaStore.Files;
37 import android.provider.MediaStore.MediaColumns;
38 import android.system.ErrnoException;
39 import android.system.Os;
40 import android.system.OsConstants;
41 import android.util.Log;
42 import android.view.Display;
43 import android.view.WindowManager;
44 
45 import dalvik.system.CloseGuard;
46 
47 import com.google.android.collect.Sets;
48 
49 import java.io.File;
50 import java.nio.file.Path;
51 import java.nio.file.Paths;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.HashMap;
55 import java.util.Iterator;
56 import java.util.Locale;
57 import java.util.concurrent.atomic.AtomicBoolean;
58 import java.util.stream.IntStream;
59 import java.util.stream.Stream;
60 
61 /**
62  * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses
63  * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File
64  * operations are also reflected in MediaProvider if possible.
65  * operations
66  * {@hide}
67  */
68 public class MtpDatabase implements AutoCloseable {
69     private static final String TAG = MtpDatabase.class.getSimpleName();
70 
71     private final Context mContext;
72     private final ContentProviderClient mMediaProvider;
73     private final String mVolumeName;
74     private final Uri mObjectsUri;
75     private final MediaScanner mMediaScanner;
76 
77     private final AtomicBoolean mClosed = new AtomicBoolean();
78     private final CloseGuard mCloseGuard = CloseGuard.get();
79 
80     private final HashMap<String, MtpStorage> mStorageMap = new HashMap<>();
81 
82     // cached property groups for single properties
83     private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty = new HashMap<>();
84 
85     // cached property groups for all properties for a given format
86     private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat = new HashMap<>();
87 
88     // SharedPreferences for writable MTP device properties
89     private SharedPreferences mDeviceProperties;
90 
91     // Cached device properties
92     private int mBatteryLevel;
93     private int mBatteryScale;
94     private int mDeviceType;
95 
96     private MtpServer mServer;
97     private MtpStorageManager mManager;
98 
99     private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
100     private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID};
101     private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA};
102     private static final String NO_MEDIA = ".nomedia";
103 
104     static {
105         System.loadLibrary("media_jni");
106     }
107 
108     private static final int[] PLAYBACK_FORMATS = {
109             // allow transferring arbitrary files
110             MtpConstants.FORMAT_UNDEFINED,
111 
112             MtpConstants.FORMAT_ASSOCIATION,
113             MtpConstants.FORMAT_TEXT,
114             MtpConstants.FORMAT_HTML,
115             MtpConstants.FORMAT_WAV,
116             MtpConstants.FORMAT_MP3,
117             MtpConstants.FORMAT_MPEG,
118             MtpConstants.FORMAT_EXIF_JPEG,
119             MtpConstants.FORMAT_TIFF_EP,
120             MtpConstants.FORMAT_BMP,
121             MtpConstants.FORMAT_GIF,
122             MtpConstants.FORMAT_JFIF,
123             MtpConstants.FORMAT_PNG,
124             MtpConstants.FORMAT_TIFF,
125             MtpConstants.FORMAT_WMA,
126             MtpConstants.FORMAT_OGG,
127             MtpConstants.FORMAT_AAC,
128             MtpConstants.FORMAT_MP4_CONTAINER,
129             MtpConstants.FORMAT_MP2,
130             MtpConstants.FORMAT_3GP_CONTAINER,
131             MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
132             MtpConstants.FORMAT_WPL_PLAYLIST,
133             MtpConstants.FORMAT_M3U_PLAYLIST,
134             MtpConstants.FORMAT_PLS_PLAYLIST,
135             MtpConstants.FORMAT_XML_DOCUMENT,
136             MtpConstants.FORMAT_FLAC,
137             MtpConstants.FORMAT_DNG,
138             MtpConstants.FORMAT_HEIF,
139     };
140 
141     private static final int[] FILE_PROPERTIES = {
142             MtpConstants.PROPERTY_STORAGE_ID,
143             MtpConstants.PROPERTY_OBJECT_FORMAT,
144             MtpConstants.PROPERTY_PROTECTION_STATUS,
145             MtpConstants.PROPERTY_OBJECT_SIZE,
146             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
147             MtpConstants.PROPERTY_DATE_MODIFIED,
148             MtpConstants.PROPERTY_PERSISTENT_UID,
149             MtpConstants.PROPERTY_PARENT_OBJECT,
150             MtpConstants.PROPERTY_NAME,
151             MtpConstants.PROPERTY_DISPLAY_NAME,
152             MtpConstants.PROPERTY_DATE_ADDED,
153     };
154 
155     private static final int[] AUDIO_PROPERTIES = {
156             MtpConstants.PROPERTY_ARTIST,
157             MtpConstants.PROPERTY_ALBUM_NAME,
158             MtpConstants.PROPERTY_ALBUM_ARTIST,
159             MtpConstants.PROPERTY_TRACK,
160             MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
161             MtpConstants.PROPERTY_DURATION,
162             MtpConstants.PROPERTY_GENRE,
163             MtpConstants.PROPERTY_COMPOSER,
164             MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
165             MtpConstants.PROPERTY_BITRATE_TYPE,
166             MtpConstants.PROPERTY_AUDIO_BITRATE,
167             MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
168             MtpConstants.PROPERTY_SAMPLE_RATE,
169     };
170 
171     private static final int[] VIDEO_PROPERTIES = {
172             MtpConstants.PROPERTY_ARTIST,
173             MtpConstants.PROPERTY_ALBUM_NAME,
174             MtpConstants.PROPERTY_DURATION,
175             MtpConstants.PROPERTY_DESCRIPTION,
176     };
177 
178     private static final int[] IMAGE_PROPERTIES = {
179             MtpConstants.PROPERTY_DESCRIPTION,
180     };
181 
182     private static final int[] DEVICE_PROPERTIES = {
183             MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
184             MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
185             MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
186             MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
187             MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
188     };
189 
getSupportedObjectProperties(int format)190     private int[] getSupportedObjectProperties(int format) {
191         switch (format) {
192             case MtpConstants.FORMAT_MP3:
193             case MtpConstants.FORMAT_WAV:
194             case MtpConstants.FORMAT_WMA:
195             case MtpConstants.FORMAT_OGG:
196             case MtpConstants.FORMAT_AAC:
197                 return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
198                         Arrays.stream(AUDIO_PROPERTIES)).toArray();
199             case MtpConstants.FORMAT_MPEG:
200             case MtpConstants.FORMAT_3GP_CONTAINER:
201             case MtpConstants.FORMAT_WMV:
202                 return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
203                         Arrays.stream(VIDEO_PROPERTIES)).toArray();
204             case MtpConstants.FORMAT_EXIF_JPEG:
205             case MtpConstants.FORMAT_GIF:
206             case MtpConstants.FORMAT_PNG:
207             case MtpConstants.FORMAT_BMP:
208             case MtpConstants.FORMAT_DNG:
209             case MtpConstants.FORMAT_HEIF:
210                 return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
211                         Arrays.stream(IMAGE_PROPERTIES)).toArray();
212             default:
213                 return FILE_PROPERTIES;
214         }
215     }
216 
getSupportedDeviceProperties()217     private int[] getSupportedDeviceProperties() {
218         return DEVICE_PROPERTIES;
219     }
220 
getSupportedPlaybackFormats()221     private int[] getSupportedPlaybackFormats() {
222         return PLAYBACK_FORMATS;
223     }
224 
getSupportedCaptureFormats()225     private int[] getSupportedCaptureFormats() {
226         // no capture formats yet
227         return null;
228     }
229 
230     private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
231         @Override
232         public void onReceive(Context context, Intent intent) {
233             String action = intent.getAction();
234             if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
235                 mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
236                 int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
237                 if (newLevel != mBatteryLevel) {
238                     mBatteryLevel = newLevel;
239                     if (mServer != null) {
240                         // send device property changed event
241                         mServer.sendDevicePropertyChanged(
242                                 MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
243                     }
244                 }
245             }
246         }
247     };
248 
MtpDatabase(Context context, String volumeName, String[] subDirectories)249     public MtpDatabase(Context context, String volumeName,
250             String[] subDirectories) {
251         native_setup();
252         mContext = context;
253         mMediaProvider = context.getContentResolver()
254                 .acquireContentProviderClient(MediaStore.AUTHORITY);
255         mVolumeName = volumeName;
256         mObjectsUri = Files.getMtpObjectsUri(volumeName);
257         mMediaScanner = new MediaScanner(context, mVolumeName);
258         mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
259             @Override
260             public void sendObjectAdded(int id) {
261                 if (MtpDatabase.this.mServer != null)
262                     MtpDatabase.this.mServer.sendObjectAdded(id);
263             }
264 
265             @Override
266             public void sendObjectRemoved(int id) {
267                 if (MtpDatabase.this.mServer != null)
268                     MtpDatabase.this.mServer.sendObjectRemoved(id);
269             }
270         }, subDirectories == null ? null : Sets.newHashSet(subDirectories));
271 
272         initDeviceProperties(context);
273         mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
274         mCloseGuard.open("close");
275     }
276 
setServer(MtpServer server)277     public void setServer(MtpServer server) {
278         mServer = server;
279         // always unregister before registering
280         try {
281             mContext.unregisterReceiver(mBatteryReceiver);
282         } catch (IllegalArgumentException e) {
283             // wasn't previously registered, ignore
284         }
285         // register for battery notifications when we are connected
286         if (server != null) {
287             mContext.registerReceiver(mBatteryReceiver,
288                     new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
289         }
290     }
291 
292     @Override
close()293     public void close() {
294         mManager.close();
295         mCloseGuard.close();
296         if (mClosed.compareAndSet(false, true)) {
297             mMediaScanner.close();
298             if (mMediaProvider != null) {
299                 mMediaProvider.close();
300             }
301             native_finalize();
302         }
303     }
304 
305     @Override
finalize()306     protected void finalize() throws Throwable {
307         try {
308             if (mCloseGuard != null) {
309                 mCloseGuard.warnIfOpen();
310             }
311             close();
312         } finally {
313             super.finalize();
314         }
315     }
316 
addStorage(StorageVolume storage)317     public void addStorage(StorageVolume storage) {
318         MtpStorage mtpStorage = mManager.addMtpStorage(storage);
319         mStorageMap.put(storage.getPath(), mtpStorage);
320         if (mServer != null) {
321             mServer.addStorage(mtpStorage);
322         }
323     }
324 
removeStorage(StorageVolume storage)325     public void removeStorage(StorageVolume storage) {
326         MtpStorage mtpStorage = mStorageMap.get(storage.getPath());
327         if (mtpStorage == null) {
328             return;
329         }
330         if (mServer != null) {
331             mServer.removeStorage(mtpStorage);
332         }
333         mManager.removeMtpStorage(mtpStorage);
334         mStorageMap.remove(storage.getPath());
335     }
336 
initDeviceProperties(Context context)337     private void initDeviceProperties(Context context) {
338         final String devicePropertiesName = "device-properties";
339         mDeviceProperties = context.getSharedPreferences(devicePropertiesName,
340                 Context.MODE_PRIVATE);
341         File databaseFile = context.getDatabasePath(devicePropertiesName);
342 
343         if (databaseFile.exists()) {
344             // for backward compatibility - read device properties from sqlite database
345             // and migrate them to shared prefs
346             SQLiteDatabase db = null;
347             Cursor c = null;
348             try {
349                 db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
350                 if (db != null) {
351                     c = db.query("properties", new String[]{"_id", "code", "value"},
352                             null, null, null, null, null);
353                     if (c != null) {
354                         SharedPreferences.Editor e = mDeviceProperties.edit();
355                         while (c.moveToNext()) {
356                             String name = c.getString(1);
357                             String value = c.getString(2);
358                             e.putString(name, value);
359                         }
360                         e.commit();
361                     }
362                 }
363             } catch (Exception e) {
364                 Log.e(TAG, "failed to migrate device properties", e);
365             } finally {
366                 if (c != null) c.close();
367                 if (db != null) db.close();
368             }
369             context.deleteDatabase(devicePropertiesName);
370         }
371     }
372 
beginSendObject(String path, int format, int parent, int storageId)373     private int beginSendObject(String path, int format, int parent, int storageId) {
374         MtpStorageManager.MtpObject parentObj =
375                 parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
376         if (parentObj == null) {
377             return -1;
378         }
379 
380         Path objPath = Paths.get(path);
381         return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
382     }
383 
endSendObject(int handle, boolean succeeded)384     private void endSendObject(int handle, boolean succeeded) {
385         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
386         if (obj == null || !mManager.endSendObject(obj, succeeded)) {
387             Log.e(TAG, "Failed to successfully end send object");
388             return;
389         }
390         // Add the new file to MediaProvider
391         if (succeeded) {
392             String path = obj.getPath().toString();
393             int format = obj.getFormat();
394             // Get parent info from MediaProvider, since the id is different from MTP's
395             ContentValues values = new ContentValues();
396             values.put(Files.FileColumns.DATA, path);
397             values.put(Files.FileColumns.FORMAT, format);
398             values.put(Files.FileColumns.SIZE, obj.getSize());
399             values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
400             try {
401                 if (obj.getParent().isRoot()) {
402                     values.put(Files.FileColumns.PARENT, 0);
403                 } else {
404                     int parentId = findInMedia(obj.getParent().getPath());
405                     if (parentId != -1) {
406                         values.put(Files.FileColumns.PARENT, parentId);
407                     } else {
408                         // The parent isn't in MediaProvider. Don't add the new file.
409                         return;
410                     }
411                 }
412 
413                 Uri uri = mMediaProvider.insert(mObjectsUri, values);
414                 if (uri != null) {
415                     rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
416                 }
417             } catch (RemoteException e) {
418                 Log.e(TAG, "RemoteException in beginSendObject", e);
419             }
420         }
421     }
422 
rescanFile(String path, int handle, int format)423     private void rescanFile(String path, int handle, int format) {
424         // handle abstract playlists separately
425         // they do not exist in the file system so don't use the media scanner here
426         if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
427             // extract name from path
428             String name = path;
429             int lastSlash = name.lastIndexOf('/');
430             if (lastSlash >= 0) {
431                 name = name.substring(lastSlash + 1);
432             }
433             // strip trailing ".pla" from the name
434             if (name.endsWith(".pla")) {
435                 name = name.substring(0, name.length() - 4);
436             }
437 
438             ContentValues values = new ContentValues(1);
439             values.put(Audio.Playlists.DATA, path);
440             values.put(Audio.Playlists.NAME, name);
441             values.put(Files.FileColumns.FORMAT, format);
442             values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
443             values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
444             try {
445                 mMediaProvider.insert(
446                         Audio.Playlists.EXTERNAL_CONTENT_URI, values);
447             } catch (RemoteException e) {
448                 Log.e(TAG, "RemoteException in endSendObject", e);
449             }
450         } else {
451             mMediaScanner.scanMtpFile(path, handle, format);
452         }
453     }
454 
getObjectList(int storageID, int format, int parent)455     private int[] getObjectList(int storageID, int format, int parent) {
456         Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
457                 format, storageID);
458         if (objectStream == null) {
459             return null;
460         }
461         return objectStream.mapToInt(MtpStorageManager.MtpObject::getId).toArray();
462     }
463 
getNumObjects(int storageID, int format, int parent)464     private int getNumObjects(int storageID, int format, int parent) {
465         Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
466                 format, storageID);
467         if (objectStream == null) {
468             return -1;
469         }
470         return (int) objectStream.count();
471     }
472 
getObjectPropertyList(int handle, int format, int property, int groupCode, int depth)473     private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
474             int groupCode, int depth) {
475         // FIXME - implement group support
476         if (property == 0) {
477             if (groupCode == 0) {
478                 return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED);
479             }
480             return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
481         }
482         if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) {
483             // request all objects starting at root
484             handle = 0xFFFFFFFF;
485             depth = 0;
486         }
487         if (!(depth == 0 || depth == 1)) {
488             // we only support depth 0 and 1
489             // depth 0: single object, depth 1: immediate children
490             return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
491         }
492         Stream<MtpStorageManager.MtpObject> objectStream = Stream.of();
493         if (handle == 0xFFFFFFFF) {
494             // All objects are requested
495             objectStream = mManager.getObjects(0, format, 0xFFFFFFFF);
496             if (objectStream == null) {
497                 return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
498             }
499         } else if (handle != 0) {
500             // Add the requested object if format matches
501             MtpStorageManager.MtpObject obj = mManager.getObject(handle);
502             if (obj == null) {
503                 return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
504             }
505             if (obj.getFormat() == format || format == 0) {
506                 objectStream = Stream.of(obj);
507             }
508         }
509         if (handle == 0 || depth == 1) {
510             if (handle == 0) {
511                 handle = 0xFFFFFFFF;
512             }
513             // Get the direct children of root or this object.
514             Stream<MtpStorageManager.MtpObject> childStream = mManager.getObjects(handle, format,
515                     0xFFFFFFFF);
516             if (childStream == null) {
517                 return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
518             }
519             objectStream = Stream.concat(objectStream, childStream);
520         }
521 
522         MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK);
523         MtpPropertyGroup propertyGroup;
524         Iterator<MtpStorageManager.MtpObject> iter = objectStream.iterator();
525         while (iter.hasNext()) {
526             MtpStorageManager.MtpObject obj = iter.next();
527             if (property == 0xffffffff) {
528                 // Get all properties supported by this object
529                 propertyGroup = mPropertyGroupsByFormat.get(obj.getFormat());
530                 if (propertyGroup == null) {
531                     int[] propertyList = getSupportedObjectProperties(format);
532                     propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
533                             propertyList);
534                     mPropertyGroupsByFormat.put(format, propertyGroup);
535                 }
536             } else {
537                 // Get this property value
538                 final int[] propertyList = new int[]{property};
539                 propertyGroup = mPropertyGroupsByProperty.get(property);
540                 if (propertyGroup == null) {
541                     propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
542                             propertyList);
543                     mPropertyGroupsByProperty.put(property, propertyGroup);
544                 }
545             }
546             int err = propertyGroup.getPropertyList(obj, ret);
547             if (err != MtpConstants.RESPONSE_OK) {
548                 return new MtpPropertyList(err);
549             }
550         }
551         return ret;
552     }
553 
renameFile(int handle, String newName)554     private int renameFile(int handle, String newName) {
555         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
556         if (obj == null) {
557             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
558         }
559         Path oldPath = obj.getPath();
560 
561         // now rename the file.  make sure this succeeds before updating database
562         if (!mManager.beginRenameObject(obj, newName))
563             return MtpConstants.RESPONSE_GENERAL_ERROR;
564         Path newPath = obj.getPath();
565         boolean success = oldPath.toFile().renameTo(newPath.toFile());
566         try {
567             Os.access(oldPath.toString(), OsConstants.F_OK);
568             Os.access(newPath.toString(), OsConstants.F_OK);
569         } catch (ErrnoException e) {
570             // Ignore. Could fail if the metadata was already updated.
571         }
572 
573         if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) {
574             Log.e(TAG, "Failed to end rename object");
575         }
576         if (!success) {
577             return MtpConstants.RESPONSE_GENERAL_ERROR;
578         }
579 
580         // finally update MediaProvider
581         ContentValues values = new ContentValues();
582         values.put(Files.FileColumns.DATA, newPath.toString());
583         String[] whereArgs = new String[]{oldPath.toString()};
584         try {
585             // note - we are relying on a special case in MediaProvider.update() to update
586             // the paths for all children in the case where this is a directory.
587             mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
588         } catch (RemoteException e) {
589             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
590         }
591 
592         // check if nomedia status changed
593         if (obj.isDir()) {
594             // for directories, check if renamed from something hidden to something non-hidden
595             if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) {
596                 // directory was unhidden
597                 try {
598                     mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null);
599                 } catch (RemoteException e) {
600                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
601                 }
602             }
603         } else {
604             // for files, check if renamed from .nomedia to something else
605             if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)
606                     && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) {
607                 try {
608                     mMediaProvider.call(MediaStore.UNHIDE_CALL,
609                             oldPath.getParent().toString(), null);
610                 } catch (RemoteException e) {
611                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
612                 }
613             }
614         }
615         return MtpConstants.RESPONSE_OK;
616     }
617 
beginMoveObject(int handle, int newParent, int newStorage)618     private int beginMoveObject(int handle, int newParent, int newStorage) {
619         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
620         MtpStorageManager.MtpObject parent = newParent == 0 ?
621                 mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
622         if (obj == null || parent == null)
623             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
624 
625         boolean allowed = mManager.beginMoveObject(obj, parent);
626         return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
627     }
628 
endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage, int objId, boolean success)629     private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
630             int objId, boolean success) {
631         MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
632                 mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent);
633         MtpStorageManager.MtpObject newParentObj = newParent == 0 ?
634                 mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
635         MtpStorageManager.MtpObject obj = mManager.getObject(objId);
636         String name = obj.getName();
637         if (newParentObj == null || oldParentObj == null
638                 ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) {
639             Log.e(TAG, "Failed to end move object");
640             return;
641         }
642 
643         obj = mManager.getObject(objId);
644         if (!success || obj == null)
645             return;
646         // Get parent info from MediaProvider, since the id is different from MTP's
647         ContentValues values = new ContentValues();
648         Path path = newParentObj.getPath().resolve(name);
649         Path oldPath = oldParentObj.getPath().resolve(name);
650         values.put(Files.FileColumns.DATA, path.toString());
651         if (obj.getParent().isRoot()) {
652             values.put(Files.FileColumns.PARENT, 0);
653         } else {
654             int parentId = findInMedia(path.getParent());
655             if (parentId != -1) {
656                 values.put(Files.FileColumns.PARENT, parentId);
657             } else {
658                 // The new parent isn't in MediaProvider, so delete the object instead
659                 deleteFromMedia(oldPath, obj.isDir());
660                 return;
661             }
662         }
663         // update MediaProvider
664         Cursor c = null;
665         String[] whereArgs = new String[]{oldPath.toString()};
666         try {
667             int parentId = -1;
668             if (!oldParentObj.isRoot()) {
669                 parentId = findInMedia(oldPath.getParent());
670             }
671             if (oldParentObj.isRoot() || parentId != -1) {
672                 // Old parent exists in MediaProvider - perform a move
673                 // note - we are relying on a special case in MediaProvider.update() to update
674                 // the paths for all children in the case where this is a directory.
675                 mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
676             } else {
677                 // Old parent doesn't exist - add the object
678                 values.put(Files.FileColumns.FORMAT, obj.getFormat());
679                 values.put(Files.FileColumns.SIZE, obj.getSize());
680                 values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
681                 Uri uri = mMediaProvider.insert(mObjectsUri, values);
682                 if (uri != null) {
683                     rescanFile(path.toString(),
684                             Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat());
685                 }
686             }
687         } catch (RemoteException e) {
688             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
689         }
690     }
691 
beginCopyObject(int handle, int newParent, int newStorage)692     private int beginCopyObject(int handle, int newParent, int newStorage) {
693         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
694         MtpStorageManager.MtpObject parent = newParent == 0 ?
695                 mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
696         if (obj == null || parent == null)
697             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
698         return mManager.beginCopyObject(obj, parent);
699     }
700 
endCopyObject(int handle, boolean success)701     private void endCopyObject(int handle, boolean success) {
702         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
703         if (obj == null || !mManager.endCopyObject(obj, success)) {
704             Log.e(TAG, "Failed to end copy object");
705             return;
706         }
707         if (!success) {
708             return;
709         }
710         String path = obj.getPath().toString();
711         int format = obj.getFormat();
712         // Get parent info from MediaProvider, since the id is different from MTP's
713         ContentValues values = new ContentValues();
714         values.put(Files.FileColumns.DATA, path);
715         values.put(Files.FileColumns.FORMAT, format);
716         values.put(Files.FileColumns.SIZE, obj.getSize());
717         values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
718         try {
719             if (obj.getParent().isRoot()) {
720                 values.put(Files.FileColumns.PARENT, 0);
721             } else {
722                 int parentId = findInMedia(obj.getParent().getPath());
723                 if (parentId != -1) {
724                     values.put(Files.FileColumns.PARENT, parentId);
725                 } else {
726                     // The parent isn't in MediaProvider. Don't add the new file.
727                     return;
728                 }
729             }
730             if (obj.isDir()) {
731                 mMediaScanner.scanDirectories(new String[]{path});
732             } else {
733                 Uri uri = mMediaProvider.insert(mObjectsUri, values);
734                 if (uri != null) {
735                     rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
736                 }
737             }
738         } catch (RemoteException e) {
739             Log.e(TAG, "RemoteException in beginSendObject", e);
740         }
741     }
742 
setObjectProperty(int handle, int property, long intValue, String stringValue)743     private int setObjectProperty(int handle, int property,
744             long intValue, String stringValue) {
745         switch (property) {
746             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
747                 return renameFile(handle, stringValue);
748 
749             default:
750                 return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
751         }
752     }
753 
getDeviceProperty(int property, long[] outIntValue, char[] outStringValue)754     private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
755         switch (property) {
756             case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
757             case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
758                 // writable string properties kept in shared preferences
759                 String value = mDeviceProperties.getString(Integer.toString(property), "");
760                 int length = value.length();
761                 if (length > 255) {
762                     length = 255;
763                 }
764                 value.getChars(0, length, outStringValue, 0);
765                 outStringValue[length] = 0;
766                 return MtpConstants.RESPONSE_OK;
767             case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
768                 // use screen size as max image size
769                 Display display = ((WindowManager) mContext.getSystemService(
770                         Context.WINDOW_SERVICE)).getDefaultDisplay();
771                 int width = display.getMaximumSizeDimension();
772                 int height = display.getMaximumSizeDimension();
773                 String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
774                 imageSize.getChars(0, imageSize.length(), outStringValue, 0);
775                 outStringValue[imageSize.length()] = 0;
776                 return MtpConstants.RESPONSE_OK;
777             case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
778                 outIntValue[0] = mDeviceType;
779                 return MtpConstants.RESPONSE_OK;
780             case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL:
781                 outIntValue[0] = mBatteryLevel;
782                 outIntValue[1] = mBatteryScale;
783                 return MtpConstants.RESPONSE_OK;
784             default:
785                 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
786         }
787     }
788 
setDeviceProperty(int property, long intValue, String stringValue)789     private int setDeviceProperty(int property, long intValue, String stringValue) {
790         switch (property) {
791             case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
792             case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
793                 // writable string properties kept in shared prefs
794                 SharedPreferences.Editor e = mDeviceProperties.edit();
795                 e.putString(Integer.toString(property), stringValue);
796                 return (e.commit() ? MtpConstants.RESPONSE_OK
797                         : MtpConstants.RESPONSE_GENERAL_ERROR);
798         }
799 
800         return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
801     }
802 
getObjectInfo(int handle, int[] outStorageFormatParent, char[] outName, long[] outCreatedModified)803     private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
804             char[] outName, long[] outCreatedModified) {
805         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
806         if (obj == null) {
807             return false;
808         }
809         outStorageFormatParent[0] = obj.getStorageId();
810         outStorageFormatParent[1] = obj.getFormat();
811         outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId();
812 
813         int nameLen = Integer.min(obj.getName().length(), 255);
814         obj.getName().getChars(0, nameLen, outName, 0);
815         outName[nameLen] = 0;
816 
817         outCreatedModified[0] = obj.getModifiedTime();
818         outCreatedModified[1] = obj.getModifiedTime();
819         return true;
820     }
821 
getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat)822     private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
823         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
824         if (obj == null) {
825             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
826         }
827 
828         String path = obj.getPath().toString();
829         int pathLen = Integer.min(path.length(), 4096);
830         path.getChars(0, pathLen, outFilePath, 0);
831         outFilePath[pathLen] = 0;
832 
833         outFileLengthFormat[0] = obj.getSize();
834         outFileLengthFormat[1] = obj.getFormat();
835         return MtpConstants.RESPONSE_OK;
836     }
837 
getObjectFormat(int handle)838     private int getObjectFormat(int handle) {
839         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
840         if (obj == null) {
841             return -1;
842         }
843         return obj.getFormat();
844     }
845 
beginDeleteObject(int handle)846     private int beginDeleteObject(int handle) {
847         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
848         if (obj == null) {
849             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
850         }
851         if (!mManager.beginRemoveObject(obj)) {
852             return MtpConstants.RESPONSE_GENERAL_ERROR;
853         }
854         return MtpConstants.RESPONSE_OK;
855     }
856 
endDeleteObject(int handle, boolean success)857     private void endDeleteObject(int handle, boolean success) {
858         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
859         if (obj == null) {
860             return;
861         }
862         if (!mManager.endRemoveObject(obj, success))
863             Log.e(TAG, "Failed to end remove object");
864         if (success)
865             deleteFromMedia(obj.getPath(), obj.isDir());
866     }
867 
findInMedia(Path path)868     private int findInMedia(Path path) {
869         int ret = -1;
870         Cursor c = null;
871         try {
872             c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
873                     new String[]{path.toString()}, null, null);
874             if (c != null && c.moveToNext()) {
875                 ret = c.getInt(0);
876             }
877         } catch (RemoteException e) {
878             Log.e(TAG, "Error finding " + path + " in MediaProvider");
879         } finally {
880             if (c != null)
881                 c.close();
882         }
883         return ret;
884     }
885 
deleteFromMedia(Path path, boolean isDir)886     private void deleteFromMedia(Path path, boolean isDir) {
887         try {
888             // Delete the object(s) from MediaProvider, but ignore errors.
889             if (isDir) {
890                 // recursive case - delete all children first
891                 mMediaProvider.delete(mObjectsUri,
892                         // the 'like' makes it use the index, the 'lower()' makes it correct
893                         // when the path contains sqlite wildcard characters
894                         "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
895                         new String[]{path + "/%", Integer.toString(path.toString().length() + 1),
896                                 path.toString() + "/"});
897             }
898 
899             String[] whereArgs = new String[]{path.toString()};
900             if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) {
901                 if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) {
902                     try {
903                         String parentPath = path.getParent().toString();
904                         mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null);
905                     } catch (RemoteException e) {
906                         Log.e(TAG, "failed to unhide/rescan for " + path);
907                     }
908                 }
909             } else {
910                 Log.i(TAG, "Mediaprovider didn't delete " + path);
911             }
912         } catch (Exception e) {
913             Log.d(TAG, "Failed to delete " + path + " from MediaProvider");
914         }
915     }
916 
getObjectReferences(int handle)917     private int[] getObjectReferences(int handle) {
918         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
919         if (obj == null)
920             return null;
921         // Translate this handle to the MediaProvider Handle
922         handle = findInMedia(obj.getPath());
923         if (handle == -1)
924             return null;
925         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
926         Cursor c = null;
927         try {
928             c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null);
929             if (c == null) {
930                 return null;
931             }
932                 ArrayList<Integer> result = new ArrayList<>();
933                 while (c.moveToNext()) {
934                     // Translate result handles back into handles for this session.
935                     String refPath = c.getString(0);
936                     MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath);
937                     if (refObj != null) {
938                         result.add(refObj.getId());
939                     }
940                 }
941                 return result.stream().mapToInt(Integer::intValue).toArray();
942         } catch (RemoteException e) {
943             Log.e(TAG, "RemoteException in getObjectList", e);
944         } finally {
945             if (c != null) {
946                 c.close();
947             }
948         }
949         return null;
950     }
951 
setObjectReferences(int handle, int[] references)952     private int setObjectReferences(int handle, int[] references) {
953         MtpStorageManager.MtpObject obj = mManager.getObject(handle);
954         if (obj == null)
955             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
956         // Translate this handle to the MediaProvider Handle
957         handle = findInMedia(obj.getPath());
958         if (handle == -1)
959             return MtpConstants.RESPONSE_GENERAL_ERROR;
960         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
961         ArrayList<ContentValues> valuesList = new ArrayList<>();
962         for (int id : references) {
963             // Translate each reference id to the MediaProvider Id
964             MtpStorageManager.MtpObject refObj = mManager.getObject(id);
965             if (refObj == null)
966                 continue;
967             int refHandle = findInMedia(refObj.getPath());
968             if (refHandle == -1)
969                 continue;
970             ContentValues values = new ContentValues();
971             values.put(Files.FileColumns._ID, refHandle);
972             valuesList.add(values);
973         }
974         try {
975             if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) {
976                 return MtpConstants.RESPONSE_OK;
977             }
978         } catch (RemoteException e) {
979             Log.e(TAG, "RemoteException in setObjectReferences", e);
980         }
981         return MtpConstants.RESPONSE_GENERAL_ERROR;
982     }
983 
984     // used by the JNI code
985     private long mNativeContext;
986 
native_setup()987     private native final void native_setup();
native_finalize()988     private native final void native_finalize();
989 }
990