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