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