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