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