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