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