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