content: /*
   * Copyright (C) 2010 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.mtp;
  
  import android.content.BroadcastReceiver;
  import android.content.ContentProviderClient;
  import android.content.ContentValues;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.SharedPreferences;
  import android.database.Cursor;
  import android.database.sqlite.SQLiteDatabase;
  import android.net.Uri;
  import android.os.BatteryManager;
  import android.os.RemoteException;
  import android.os.SystemProperties;
  import android.os.storage.StorageVolume;
  import android.provider.MediaStore;
  import android.provider.MediaStore.Files;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.system.OsConstants;
  import android.util.Log;
  import android.util.SparseArray;
  import android.view.Display;
  import android.view.WindowManager;
  
  import com.android.internal.annotations.VisibleForNative;
  
  import dalvik.system.CloseGuard;
  
  import com.google.android.collect.Sets;
  
  import java.io.File;
  import java.nio.file.Path;
  import java.nio.file.Paths;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Locale;
  import java.util.Objects;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.stream.IntStream;
  
  /**
   * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses
   * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File
   * operations are also reflected in MediaProvider if possible.
   * operations
   * {@hide}
   */
  public class MtpDatabase implements AutoCloseable {
      private static final String TAG = MtpDatabase.class.getSimpleName();
  
      private final Context mContext;
      private final ContentProviderClient mMediaProvider;
  
      private final AtomicBoolean mClosed = new AtomicBoolean();
      private final CloseGuard mCloseGuard = CloseGuard.get();
  
      private final HashMap<String, MtpStorage> mStorageMap = new HashMap<>();
  
      // cached property groups for single properties
      private final SparseArray<MtpPropertyGroup> mPropertyGroupsByProperty = new SparseArray<>();
  
      // cached property groups for all properties for a given format
      private final SparseArray<MtpPropertyGroup> mPropertyGroupsByFormat = new SparseArray<>();
  
      // SharedPreferences for writable MTP device properties
      private SharedPreferences mDeviceProperties;
  
      // Cached device properties
      private int mBatteryLevel;
      private int mBatteryScale;
      private int mDeviceType;
  
      private MtpServer mServer;
      private MtpStorageManager mManager;
  
      private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
      private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID};
      private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA};
      private static final String NO_MEDIA = ".nomedia";
  
      static {
          System.loadLibrary("media_jni");
      }
  
      private static final int[] PLAYBACK_FORMATS = {
              // allow transferring arbitrary files
              MtpConstants.FORMAT_UNDEFINED,
  
              MtpConstants.FORMAT_ASSOCIATION,
              MtpConstants.FORMAT_TEXT,
              MtpConstants.FORMAT_HTML,
              MtpConstants.FORMAT_WAV,
              MtpConstants.FORMAT_MP3,
              MtpConstants.FORMAT_MPEG,
              MtpConstants.FORMAT_EXIF_JPEG,
              MtpConstants.FORMAT_TIFF_EP,
              MtpConstants.FORMAT_BMP,
              MtpConstants.FORMAT_GIF,
              MtpConstants.FORMAT_JFIF,
              MtpConstants.FORMAT_PNG,
              MtpConstants.FORMAT_TIFF,
              MtpConstants.FORMAT_WMA,
              MtpConstants.FORMAT_OGG,
              MtpConstants.FORMAT_AAC,
              MtpConstants.FORMAT_MP4_CONTAINER,
              MtpConstants.FORMAT_MP2,
              MtpConstants.FORMAT_3GP_CONTAINER,
              MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
              MtpConstants.FORMAT_WPL_PLAYLIST,
              MtpConstants.FORMAT_M3U_PLAYLIST,
              MtpConstants.FORMAT_PLS_PLAYLIST,
              MtpConstants.FORMAT_XML_DOCUMENT,
              MtpConstants.FORMAT_FLAC,
              MtpConstants.FORMAT_DNG,
              MtpConstants.FORMAT_HEIF,
      };
  
      private static final int[] FILE_PROPERTIES = {
              MtpConstants.PROPERTY_STORAGE_ID,
              MtpConstants.PROPERTY_OBJECT_FORMAT,
              MtpConstants.PROPERTY_PROTECTION_STATUS,
              MtpConstants.PROPERTY_OBJECT_SIZE,
              MtpConstants.PROPERTY_OBJECT_FILE_NAME,
              MtpConstants.PROPERTY_DATE_MODIFIED,
              MtpConstants.PROPERTY_PERSISTENT_UID,
              MtpConstants.PROPERTY_PARENT_OBJECT,
              MtpConstants.PROPERTY_NAME,
              MtpConstants.PROPERTY_DISPLAY_NAME,
              MtpConstants.PROPERTY_DATE_ADDED,
      };
  
      private static final int[] AUDIO_PROPERTIES = {
              MtpConstants.PROPERTY_ARTIST,
              MtpConstants.PROPERTY_ALBUM_NAME,
              MtpConstants.PROPERTY_ALBUM_ARTIST,
              MtpConstants.PROPERTY_TRACK,
              MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
              MtpConstants.PROPERTY_DURATION,
              MtpConstants.PROPERTY_COMPOSER,
              MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
              MtpConstants.PROPERTY_BITRATE_TYPE,
              MtpConstants.PROPERTY_AUDIO_BITRATE,
              MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
              MtpConstants.PROPERTY_SAMPLE_RATE,
      };
  
      private static final int[] VIDEO_PROPERTIES = {
              MtpConstants.PROPERTY_ARTIST,
              MtpConstants.PROPERTY_ALBUM_NAME,
              MtpConstants.PROPERTY_DURATION,
              MtpConstants.PROPERTY_DESCRIPTION,
      };
  
      private static final int[] IMAGE_PROPERTIES = {
              MtpConstants.PROPERTY_DESCRIPTION,
      };
  
      private static final int[] DEVICE_PROPERTIES = {
              MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
              MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
              MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
              MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
              MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
      };
  
      @VisibleForNative
      private int[] getSupportedObjectProperties(int format) {
          switch (format) {
              case MtpConstants.FORMAT_MP3:
              case MtpConstants.FORMAT_WAV:
              case MtpConstants.FORMAT_WMA:
              case MtpConstants.FORMAT_OGG:
              case MtpConstants.FORMAT_AAC:
                  return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
                          Arrays.stream(AUDIO_PROPERTIES)).toArray();
              case MtpConstants.FORMAT_MPEG:
              case MtpConstants.FORMAT_3GP_CONTAINER:
              case MtpConstants.FORMAT_WMV:
                  return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
                          Arrays.stream(VIDEO_PROPERTIES)).toArray();
              case MtpConstants.FORMAT_EXIF_JPEG:
              case MtpConstants.FORMAT_GIF:
              case MtpConstants.FORMAT_PNG:
              case MtpConstants.FORMAT_BMP:
              case MtpConstants.FORMAT_DNG:
              case MtpConstants.FORMAT_HEIF:
                  return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
                          Arrays.stream(IMAGE_PROPERTIES)).toArray();
              default:
                  return FILE_PROPERTIES;
          }
      }
  
      public static Uri getObjectPropertiesUri(int format, String volumeName) {
          switch (format) {
              case MtpConstants.FORMAT_MP3:
              case MtpConstants.FORMAT_WAV:
              case MtpConstants.FORMAT_WMA:
              case MtpConstants.FORMAT_OGG:
              case MtpConstants.FORMAT_AAC:
                  return MediaStore.Audio.Media.getContentUri(volumeName);
              case MtpConstants.FORMAT_MPEG:
              case MtpConstants.FORMAT_3GP_CONTAINER:
              case MtpConstants.FORMAT_WMV:
                  return MediaStore.Video.Media.getContentUri(volumeName);
              case MtpConstants.FORMAT_EXIF_JPEG:
              case MtpConstants.FORMAT_GIF:
              case MtpConstants.FORMAT_PNG:
              case MtpConstants.FORMAT_BMP:
              case MtpConstants.FORMAT_DNG:
              case MtpConstants.FORMAT_HEIF:
                  return MediaStore.Images.Media.getContentUri(volumeName);
              default:
                  return MediaStore.Files.getContentUri(volumeName);
          }
      }
  
      @VisibleForNative
      private int[] getSupportedDeviceProperties() {
          return DEVICE_PROPERTIES;
      }
  
      @VisibleForNative
      private int[] getSupportedPlaybackFormats() {
          return PLAYBACK_FORMATS;
      }
  
      @VisibleForNative
      private int[] getSupportedCaptureFormats() {
          // no capture formats yet
          return null;
      }
  
      private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              String action = intent.getAction();
              if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
                  mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
                  int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
                  if (newLevel != mBatteryLevel) {
                      mBatteryLevel = newLevel;
                      if (mServer != null) {
                          // send device property changed event
                          mServer.sendDevicePropertyChanged(
                                  MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
                      }
                  }
              }
          }
      };
  
      public MtpDatabase(Context context, String[] subDirectories) {
          native_setup();
          mContext = Objects.requireNonNull(context);
          mMediaProvider = context.getContentResolver()
                  .acquireContentProviderClient(MediaStore.AUTHORITY);
          mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
              @Override
              public void sendObjectAdded(int id) {
                  if (MtpDatabase.this.mServer != null)
                      MtpDatabase.this.mServer.sendObjectAdded(id);
              }
  
              @Override
              public void sendObjectRemoved(int id) {
                  if (MtpDatabase.this.mServer != null)
                      MtpDatabase.this.mServer.sendObjectRemoved(id);
              }
  
              @Override
              public void sendObjectInfoChanged(int id) {
                  if (MtpDatabase.this.mServer != null)
                      MtpDatabase.this.mServer.sendObjectInfoChanged(id);
              }
          }, subDirectories == null ? null : Sets.newHashSet(subDirectories));
  
          initDeviceProperties(context);
          mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
          mCloseGuard.open("close");
      }
  
      public void setServer(MtpServer server) {
          mServer = server;
          // always unregister before registering
          try {
              mContext.unregisterReceiver(mBatteryReceiver);
          } catch (IllegalArgumentException e) {
              // wasn't previously registered, ignore
          }
          // register for battery notifications when we are connected
          if (server != null) {
              mContext.registerReceiver(mBatteryReceiver,
                      new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
          }
      }
  
      public Context getContext() {
          return mContext;
      }
  
      @Override
      public void close() {
          mManager.close();
          mCloseGuard.close();
          if (mClosed.compareAndSet(false, true)) {
              if (mMediaProvider != null) {
                  mMediaProvider.close();
              }
              native_finalize();
          }
      }
  
      @Override
      protected void finalize() throws Throwable {
          try {
              if (mCloseGuard != null) {
                  mCloseGuard.warnIfOpen();
              }
              close();
          } finally {
              super.finalize();
          }
      }
  
      public void addStorage(StorageVolume storage) {
          MtpStorage mtpStorage = mManager.addMtpStorage(storage);
          mStorageMap.put(storage.getPath(), mtpStorage);
          if (mServer != null) {
              mServer.addStorage(mtpStorage);
          }
      }
  
      public void removeStorage(StorageVolume storage) {
          MtpStorage mtpStorage = mStorageMap.get(storage.getPath());
          if (mtpStorage == null) {
              return;
          }
          if (mServer != null) {
              mServer.removeStorage(mtpStorage);
          }
          mManager.removeMtpStorage(mtpStorage);
          mStorageMap.remove(storage.getPath());
      }
  
      private void initDeviceProperties(Context context) {
          final String devicePropertiesName = "device-properties";
          mDeviceProperties = context.getSharedPreferences(devicePropertiesName,
                  Context.MODE_PRIVATE);
          File databaseFile = context.getDatabasePath(devicePropertiesName);
  
          if (databaseFile.exists()) {
              // for backward compatibility - read device properties from sqlite database
              // and migrate them to shared prefs
              SQLiteDatabase db = null;
              Cursor c = null;
              try {
                  db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
                  if (db != null) {
                      c = db.query("properties", new String[]{"_id", "code", "value"},
                              null, null, null, null, null);
                      if (c != null) {
                          SharedPreferences.Editor e = mDeviceProperties.edit();
                          while (c.moveToNext()) {
                              String name = c.getString(1);
                              String value = c.getString(2);
                              e.putString(name, value);
                          }
                          e.commit();
                      }
                  }
              } catch (Exception e) {
                  Log.e(TAG, "failed to migrate device properties", e);
              } finally {
                  if (c != null) c.close();
                  if (db != null) db.close();
              }
              context.deleteDatabase(devicePropertiesName);
          }
      }
  
      @VisibleForNative
      private int beginSendObject(String path, int format, int parent, int storageId) {
          MtpStorageManager.MtpObject parentObj =
                  parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
          if (parentObj == null) {
              return -1;
          }
  
          Path objPath = Paths.get(path);
          return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
      }
  
      @VisibleForNative
      private void endSendObject(int handle, boolean succeeded) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null || !mManager.endSendObject(obj, succeeded)) {
              Log.e(TAG, "Failed to successfully end send object");
              return;
          }
          // Add the new file to MediaProvider
          if (succeeded) {
              MediaStore.scanFile(mContext, obj.getPath().toFile());
          }
      }
  
      @VisibleForNative
      private void rescanFile(String path, int handle, int format) {
          MediaStore.scanFile(mContext, new File(path));
      }
  
      @VisibleForNative
      private int[] getObjectList(int storageID, int format, int parent) {
          List<MtpStorageManager.MtpObject> objs = mManager.getObjects(parent,
                  format, storageID);
          if (objs == null) {
              return null;
          }
          int[] ret = new int[objs.size()];
          for (int i = 0; i < objs.size(); i++) {
              ret[i] = objs.get(i).getId();
          }
          return ret;
      }
  
      @VisibleForNative
      private int getNumObjects(int storageID, int format, int parent) {
          List<MtpStorageManager.MtpObject> objs = mManager.getObjects(parent,
                  format, storageID);
          if (objs == null) {
              return -1;
          }
          return objs.size();
      }
  
      @VisibleForNative
      private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
              int groupCode, int depth) {
          // FIXME - implement group support
          if (property == 0) {
              if (groupCode == 0) {
                  return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED);
              }
              return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
          }
          if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) {
              // request all objects starting at root
              handle = 0xFFFFFFFF;
              depth = 0;
          }
          if (!(depth == 0 || depth == 1)) {
              // we only support depth 0 and 1
              // depth 0: single object, depth 1: immediate children
              return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
          }
          List<MtpStorageManager.MtpObject> objs = null;
          MtpStorageManager.MtpObject thisObj = null;
          if (handle == 0xFFFFFFFF) {
              // All objects are requested
              objs = mManager.getObjects(0, format, 0xFFFFFFFF);
              if (objs == null) {
                  return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
              }
          } else if (handle != 0) {
              // Add the requested object if format matches
              MtpStorageManager.MtpObject obj = mManager.getObject(handle);
              if (obj == null) {
                  return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
              }
              if (obj.getFormat() == format || format == 0) {
                  thisObj = obj;
              }
          }
          if (handle == 0 || depth == 1) {
              if (handle == 0) {
                  handle = 0xFFFFFFFF;
              }
              // Get the direct children of root or this object.
              objs = mManager.getObjects(handle, format,
                      0xFFFFFFFF);
              if (objs == null) {
                  return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
              }
          }
          if (objs == null) {
              objs = new ArrayList<>();
          }
          if (thisObj != null) {
              objs.add(thisObj);
          }
  
          MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK);
          MtpPropertyGroup propertyGroup;
          for (MtpStorageManager.MtpObject obj : objs) {
              if (property == 0xffffffff) {
                  if (format == 0 && handle != 0 && handle != 0xffffffff) {
                      // return properties based on the object's format
                      format = obj.getFormat();
                  }
                  // Get all properties supported by this object
                  // format should be the same between get & put
                  propertyGroup = mPropertyGroupsByFormat.get(format);
                  if (propertyGroup == null) {
                      final int[] propertyList = getSupportedObjectProperties(format);
                      propertyGroup = new MtpPropertyGroup(propertyList);
                      mPropertyGroupsByFormat.put(format, propertyGroup);
                  }
              } else {
                  // Get this property value
                  propertyGroup = mPropertyGroupsByProperty.get(property);
                  if (propertyGroup == null) {
                      final int[] propertyList = new int[]{property};
                      propertyGroup = new MtpPropertyGroup(propertyList);
                      mPropertyGroupsByProperty.put(property, propertyGroup);
                  }
              }
              int err = propertyGroup.getPropertyList(mMediaProvider, obj.getVolumeName(), obj, ret);
              if (err != MtpConstants.RESPONSE_OK) {
                  return new MtpPropertyList(err);
              }
          }
          return ret;
      }
  
      private int renameFile(int handle, String newName) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null) {
              return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
          }
          Path oldPath = obj.getPath();
  
          // now rename the file.  make sure this succeeds before updating database
          if (!mManager.beginRenameObject(obj, newName))
              return MtpConstants.RESPONSE_GENERAL_ERROR;
          Path newPath = obj.getPath();
          boolean success = oldPath.toFile().renameTo(newPath.toFile());
          try {
              Os.access(oldPath.toString(), OsConstants.F_OK);
              Os.access(newPath.toString(), OsConstants.F_OK);
          } catch (ErrnoException e) {
              // Ignore. Could fail if the metadata was already updated.
          }
  
          if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) {
              Log.e(TAG, "Failed to end rename object");
          }
          if (!success) {
              return MtpConstants.RESPONSE_GENERAL_ERROR;
          }
  
          // finally update MediaProvider
          ContentValues values = new ContentValues();
          values.put(Files.FileColumns.DATA, newPath.toString());
          String[] whereArgs = new String[]{oldPath.toString()};
          try {
              // note - we are relying on a special case in MediaProvider.update() to update
              // the paths for all children in the case where this is a directory.
              final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName());
              mMediaProvider.update(objectsUri, values, PATH_WHERE, whereArgs);
          } catch (RemoteException e) {
              Log.e(TAG, "RemoteException in mMediaProvider.update", e);
          }
  
          // check if nomedia status changed
          if (obj.isDir()) {
              // for directories, check if renamed from something hidden to something non-hidden
              if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) {
                  MediaStore.scanFile(mContext, newPath.toFile());
              }
          } else {
              // for files, check if renamed from .nomedia to something else
              if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)
                      && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) {
                  MediaStore.scanFile(mContext, newPath.getParent().toFile());
              }
          }
          return MtpConstants.RESPONSE_OK;
      }
  
      @VisibleForNative
      private int beginMoveObject(int handle, int newParent, int newStorage) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          MtpStorageManager.MtpObject parent = newParent == 0 ?
                  mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
          if (obj == null || parent == null)
              return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
  
          boolean allowed = mManager.beginMoveObject(obj, parent);
          return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
      }
  
      @VisibleForNative
      private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
              int objId, boolean success) {
          MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
                  mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent);
          MtpStorageManager.MtpObject newParentObj = newParent == 0 ?
                  mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
          MtpStorageManager.MtpObject obj = mManager.getObject(objId);
          String name = obj.getName();
          if (newParentObj == null || oldParentObj == null
                  ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) {
              Log.e(TAG, "Failed to end move object");
              return;
          }
  
          obj = mManager.getObject(objId);
          if (!success || obj == null)
              return;
          // Get parent info from MediaProvider, since the id is different from MTP's
          ContentValues values = new ContentValues();
          Path path = newParentObj.getPath().resolve(name);
          Path oldPath = oldParentObj.getPath().resolve(name);
          values.put(Files.FileColumns.DATA, path.toString());
          if (obj.getParent().isRoot()) {
              values.put(Files.FileColumns.PARENT, 0);
          } else {
              int parentId = findInMedia(newParentObj, path.getParent());
              if (parentId != -1) {
                  values.put(Files.FileColumns.PARENT, parentId);
              } else {
                  // The new parent isn't in MediaProvider, so delete the object instead
                  deleteFromMedia(obj, oldPath, obj.isDir());
                  return;
              }
          }
          // update MediaProvider
          Cursor c = null;
          String[] whereArgs = new String[]{oldPath.toString()};
          try {
              int parentId = -1;
              if (!oldParentObj.isRoot()) {
                  parentId = findInMedia(oldParentObj, oldPath.getParent());
              }
              if (oldParentObj.isRoot() || parentId != -1) {
                  // Old parent exists in MediaProvider - perform a move
                  // note - we are relying on a special case in MediaProvider.update() to update
                  // the paths for all children in the case where this is a directory.
                  final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName());
                  mMediaProvider.update(objectsUri, values, PATH_WHERE, whereArgs);
              } else {
                  // Old parent doesn't exist - add the object
                  MediaStore.scanFile(mContext, path.toFile());
              }
          } catch (RemoteException e) {
              Log.e(TAG, "RemoteException in mMediaProvider.update", e);
          }
      }
  
      @VisibleForNative
      private int beginCopyObject(int handle, int newParent, int newStorage) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          MtpStorageManager.MtpObject parent = newParent == 0 ?
                  mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
          if (obj == null || parent == null)
              return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
          return mManager.beginCopyObject(obj, parent);
      }
  
      @VisibleForNative
      private void endCopyObject(int handle, boolean success) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null || !mManager.endCopyObject(obj, success)) {
              Log.e(TAG, "Failed to end copy object");
              return;
          }
          if (!success) {
              return;
          }
          MediaStore.scanFile(mContext, obj.getPath().toFile());
      }
  
      @VisibleForNative
      private int setObjectProperty(int handle, int property,
              long intValue, String stringValue) {
          switch (property) {
              case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
                  return renameFile(handle, stringValue);
  
              default:
                  return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
          }
      }
  
      @VisibleForNative
      private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
          switch (property) {
              case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
              case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
                  // writable string properties kept in shared preferences
                  String value = mDeviceProperties.getString(Integer.toString(property), "");
                  int length = value.length();
                  if (length > 255) {
                      length = 255;
                  }
                  value.getChars(0, length, outStringValue, 0);
                  outStringValue[length] = 0;
                  return MtpConstants.RESPONSE_OK;
              case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
                  // use screen size as max image size
                  Display display = ((WindowManager) mContext.getSystemService(
                          Context.WINDOW_SERVICE)).getDefaultDisplay();
                  int width = display.getMaximumSizeDimension();
                  int height = display.getMaximumSizeDimension();
                  String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
                  imageSize.getChars(0, imageSize.length(), outStringValue, 0);
                  outStringValue[imageSize.length()] = 0;
                  return MtpConstants.RESPONSE_OK;
              case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
                  outIntValue[0] = mDeviceType;
                  return MtpConstants.RESPONSE_OK;
              case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL:
                  outIntValue[0] = mBatteryLevel;
                  outIntValue[1] = mBatteryScale;
                  return MtpConstants.RESPONSE_OK;
              default:
                  return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
          }
      }
  
      @VisibleForNative
      private int setDeviceProperty(int property, long intValue, String stringValue) {
          switch (property) {
              case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
              case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
                  // writable string properties kept in shared prefs
                  SharedPreferences.Editor e = mDeviceProperties.edit();
                  e.putString(Integer.toString(property), stringValue);
                  return (e.commit() ? MtpConstants.RESPONSE_OK
                          : MtpConstants.RESPONSE_GENERAL_ERROR);
          }
  
          return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
      }
  
      @VisibleForNative
      private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
              char[] outName, long[] outCreatedModified) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null) {
              return false;
          }
          outStorageFormatParent[0] = obj.getStorageId();
          outStorageFormatParent[1] = obj.getFormat();
          outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId();
  
          int nameLen = Integer.min(obj.getName().length(), 255);
          obj.getName().getChars(0, nameLen, outName, 0);
          outName[nameLen] = 0;
  
          outCreatedModified[0] = obj.getModifiedTime();
          outCreatedModified[1] = obj.getModifiedTime();
          return true;
      }
  
      @VisibleForNative
      private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null) {
              return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
          }
  
          String path = obj.getPath().toString();
          int pathLen = Integer.min(path.length(), 4096);
          path.getChars(0, pathLen, outFilePath, 0);
          outFilePath[pathLen] = 0;
  
          outFileLengthFormat[0] = obj.getSize();
          outFileLengthFormat[1] = obj.getFormat();
          return MtpConstants.RESPONSE_OK;
      }
  
      private int getObjectFormat(int handle) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null) {
              return -1;
          }
          return obj.getFormat();
      }
  
      @VisibleForNative
      private int beginDeleteObject(int handle) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null) {
              return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
          }
          if (!mManager.beginRemoveObject(obj)) {
              return MtpConstants.RESPONSE_GENERAL_ERROR;
          }
          return MtpConstants.RESPONSE_OK;
      }
  
      @VisibleForNative
      private void endDeleteObject(int handle, boolean success) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null) {
              return;
          }
          if (!mManager.endRemoveObject(obj, success))
              Log.e(TAG, "Failed to end remove object");
          if (success)
              deleteFromMedia(obj, obj.getPath(), obj.isDir());
      }
  
      private int findInMedia(MtpStorageManager.MtpObject obj, Path path) {
          final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName());
  
          int ret = -1;
          Cursor c = null;
          try {
              c = mMediaProvider.query(objectsUri, ID_PROJECTION, PATH_WHERE,
                      new String[]{path.toString()}, null, null);
              if (c != null && c.moveToNext()) {
                  ret = c.getInt(0);
              }
          } catch (RemoteException e) {
              Log.e(TAG, "Error finding " + path + " in MediaProvider");
          } finally {
              if (c != null)
                  c.close();
          }
          return ret;
      }
  
      private void deleteFromMedia(MtpStorageManager.MtpObject obj, Path path, boolean isDir) {
          final Uri objectsUri = MediaStore.Files.getMtpObjectsUri(obj.getVolumeName());
          try {
              // Delete the object(s) from MediaProvider, but ignore errors.
              if (isDir) {
                  // recursive case - delete all children first
                  mMediaProvider.delete(objectsUri,
                          // the 'like' makes it use the index, the 'lower()' makes it correct
                          // when the path contains sqlite wildcard characters
                          "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
                          new String[]{path + "/%", Integer.toString(path.toString().length() + 1),
                                  path.toString() + "/"});
              }
  
              String[] whereArgs = new String[]{path.toString()};
              if (mMediaProvider.delete(objectsUri, PATH_WHERE, whereArgs) > 0) {
                  if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) {
                      MediaStore.scanFile(mContext, path.getParent().toFile());
                  }
              } else {
                  Log.i(TAG, "Mediaprovider didn't delete " + path);
              }
          } catch (Exception e) {
              Log.d(TAG, "Failed to delete " + path + " from MediaProvider");
          }
      }
  
      @VisibleForNative
      private int[] getObjectReferences(int handle) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null)
              return null;
          // Translate this handle to the MediaProvider Handle
          handle = findInMedia(obj, obj.getPath());
          if (handle == -1)
              return null;
          Uri uri = Files.getMtpReferencesUri(obj.getVolumeName(), handle);
          Cursor c = null;
          try {
              c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null);
              if (c == null) {
                  return null;
              }
                  ArrayList<Integer> result = new ArrayList<>();
                  while (c.moveToNext()) {
                      // Translate result handles back into handles for this session.
                      String refPath = c.getString(0);
                      MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath);
                      if (refObj != null) {
                          result.add(refObj.getId());
                      }
                  }
                  return result.stream().mapToInt(Integer::intValue).toArray();
          } catch (RemoteException e) {
              Log.e(TAG, "RemoteException in getObjectList", e);
          } finally {
              if (c != null) {
                  c.close();
              }
          }
          return null;
      }
  
      @VisibleForNative
      private int setObjectReferences(int handle, int[] references) {
          MtpStorageManager.MtpObject obj = mManager.getObject(handle);
          if (obj == null)
              return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
          // Translate this handle to the MediaProvider Handle
          handle = findInMedia(obj, obj.getPath());
          if (handle == -1)
              return MtpConstants.RESPONSE_GENERAL_ERROR;
          Uri uri = Files.getMtpReferencesUri(obj.getVolumeName(), handle);
          ArrayList<ContentValues> valuesList = new ArrayList<>();
          for (int id : references) {
              // Translate each reference id to the MediaProvider Id
              MtpStorageManager.MtpObject refObj = mManager.getObject(id);
              if (refObj == null)
                  continue;
              int refHandle = findInMedia(refObj, refObj.getPath());
              if (refHandle == -1)
                  continue;
              ContentValues values = new ContentValues();
              values.put(Files.FileColumns._ID, refHandle);
              valuesList.add(values);
          }
          try {
              if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) {
                  return MtpConstants.RESPONSE_OK;
              }
          } catch (RemoteException e) {
              Log.e(TAG, "RemoteException in setObjectReferences", e);
          }
          return MtpConstants.RESPONSE_GENERAL_ERROR;
      }
  
      @VisibleForNative
      private long mNativeContext;
  
      private native final void native_setup();
      private native final void native_finalize();
  }
  