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