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