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