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