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