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