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