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