1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.mtp; 18 19 import android.content.BroadcastReceiver; 20 import android.content.ContentProviderClient; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.SharedPreferences; 26 import android.database.Cursor; 27 import android.database.sqlite.SQLiteDatabase; 28 import android.media.MediaScanner; 29 import android.net.Uri; 30 import android.os.BatteryManager; 31 import android.os.RemoteException; 32 import android.os.SystemProperties; 33 import android.provider.MediaStore; 34 import android.provider.MediaStore.Audio; 35 import android.provider.MediaStore.Files; 36 import android.provider.MediaStore.MediaColumns; 37 import android.util.Log; 38 import android.view.Display; 39 import android.view.WindowManager; 40 41 import dalvik.system.CloseGuard; 42 43 import java.io.File; 44 import java.io.IOException; 45 import java.util.HashMap; 46 import java.util.Locale; 47 import java.util.concurrent.atomic.AtomicBoolean; 48 49 /** 50 * {@hide} 51 */ 52 public class MtpDatabase implements AutoCloseable { 53 private static final String TAG = "MtpDatabase"; 54 55 private final Context mUserContext; 56 private final Context mContext; 57 private final String mPackageName; 58 private final ContentProviderClient mMediaProvider; 59 private final String mVolumeName; 60 private final Uri mObjectsUri; 61 private final MediaScanner mMediaScanner; 62 63 private final AtomicBoolean mClosed = new AtomicBoolean(); 64 private final CloseGuard mCloseGuard = CloseGuard.get(); 65 66 // path to primary storage 67 private final String mMediaStoragePath; 68 // if not null, restrict all queries to these subdirectories 69 private final String[] mSubDirectories; 70 // where clause for restricting queries to files in mSubDirectories 71 private String mSubDirectoriesWhere; 72 // where arguments for restricting queries to files in mSubDirectories 73 private String[] mSubDirectoriesWhereArgs; 74 75 private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>(); 76 77 // cached property groups for single properties 78 private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty 79 = new HashMap<Integer, MtpPropertyGroup>(); 80 81 // cached property groups for all properties for a given format 82 private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat 83 = new HashMap<Integer, MtpPropertyGroup>(); 84 85 // true if the database has been modified in the current MTP session 86 private boolean mDatabaseModified; 87 88 // SharedPreferences for writable MTP device properties 89 private SharedPreferences mDeviceProperties; 90 private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1; 91 92 private static final String[] ID_PROJECTION = new String[] { 93 Files.FileColumns._ID, // 0 94 }; 95 private static final String[] PATH_PROJECTION = new String[] { 96 Files.FileColumns._ID, // 0 97 Files.FileColumns.DATA, // 1 98 }; 99 private static final String[] FORMAT_PROJECTION = new String[] { 100 Files.FileColumns._ID, // 0 101 Files.FileColumns.FORMAT, // 1 102 }; 103 private static final String[] PATH_FORMAT_PROJECTION = new String[] { 104 Files.FileColumns._ID, // 0 105 Files.FileColumns.DATA, // 1 106 Files.FileColumns.FORMAT, // 2 107 }; 108 private static final String[] OBJECT_INFO_PROJECTION = new String[] { 109 Files.FileColumns._ID, // 0 110 Files.FileColumns.STORAGE_ID, // 1 111 Files.FileColumns.FORMAT, // 2 112 Files.FileColumns.PARENT, // 3 113 Files.FileColumns.DATA, // 4 114 Files.FileColumns.DATE_ADDED, // 5 115 Files.FileColumns.DATE_MODIFIED, // 6 116 }; 117 private static final String ID_WHERE = Files.FileColumns._ID + "=?"; 118 private static final String PATH_WHERE = Files.FileColumns.DATA + "=?"; 119 120 private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?"; 121 private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?"; 122 private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?"; 123 private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND " 124 + Files.FileColumns.FORMAT + "=?"; 125 private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND " 126 + Files.FileColumns.PARENT + "=?"; 127 private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND " 128 + Files.FileColumns.PARENT + "=?"; 129 private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND " 130 + Files.FileColumns.PARENT + "=?"; 131 132 private MtpServer mServer; 133 134 // read from native code 135 private int mBatteryLevel; 136 private int mBatteryScale; 137 138 private int mDeviceType; 139 140 static { 141 System.loadLibrary("media_jni"); 142 } 143 144 private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { 145 @Override 146 public void onReceive(Context context, Intent intent) { 147 String action = intent.getAction(); 148 if (action.equals(Intent.ACTION_BATTERY_CHANGED)) { 149 mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); 150 int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); 151 if (newLevel != mBatteryLevel) { 152 mBatteryLevel = newLevel; 153 if (mServer != null) { 154 // send device property changed event 155 mServer.sendDevicePropertyChanged( 156 MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL); 157 } 158 } 159 } 160 } 161 }; 162 163 public MtpDatabase(Context context, Context userContext, String volumeName, String storagePath, 164 String[] subDirectories) { 165 native_setup(); 166 167 mContext = context; 168 mUserContext = userContext; 169 mPackageName = context.getPackageName(); 170 mMediaProvider = userContext.getContentResolver() 171 .acquireContentProviderClient(MediaStore.AUTHORITY); 172 mVolumeName = volumeName; 173 mMediaStoragePath = storagePath; 174 mObjectsUri = Files.getMtpObjectsUri(volumeName); 175 mMediaScanner = new MediaScanner(context, mVolumeName); 176 177 mSubDirectories = subDirectories; 178 if (subDirectories != null) { 179 // Compute "where" string for restricting queries to subdirectories 180 StringBuilder builder = new StringBuilder(); 181 builder.append("("); 182 int count = subDirectories.length; 183 for (int i = 0; i < count; i++) { 184 builder.append(Files.FileColumns.DATA + "=? OR " 185 + Files.FileColumns.DATA + " LIKE ?"); 186 if (i != count - 1) { 187 builder.append(" OR "); 188 } 189 } 190 builder.append(")"); 191 mSubDirectoriesWhere = builder.toString(); 192 193 // Compute "where" arguments for restricting queries to subdirectories 194 mSubDirectoriesWhereArgs = new String[count * 2]; 195 for (int i = 0, j = 0; i < count; i++) { 196 String path = subDirectories[i]; 197 mSubDirectoriesWhereArgs[j++] = path; 198 mSubDirectoriesWhereArgs[j++] = path + "/%"; 199 } 200 } 201 202 initDeviceProperties(context); 203 mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0); 204 205 mCloseGuard.open("close"); 206 } 207 208 public void setServer(MtpServer server) { 209 mServer = server; 210 211 // always unregister before registering 212 try { 213 mContext.unregisterReceiver(mBatteryReceiver); 214 } catch (IllegalArgumentException e) { 215 // wasn't previously registered, ignore 216 } 217 218 // register for battery notifications when we are connected 219 if (server != null) { 220 mContext.registerReceiver(mBatteryReceiver, 221 new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); 222 } 223 } 224 225 @Override 226 public void close() { 227 mCloseGuard.close(); 228 if (mClosed.compareAndSet(false, true)) { 229 mMediaScanner.close(); 230 mMediaProvider.close(); 231 native_finalize(); 232 } 233 } 234 235 @Override 236 protected void finalize() throws Throwable { 237 try { 238 if (mCloseGuard != null) { 239 mCloseGuard.warnIfOpen(); 240 } 241 242 close(); 243 } finally { 244 super.finalize(); 245 } 246 } 247 248 public void addStorage(MtpStorage storage) { 249 mStorageMap.put(storage.getPath(), storage); 250 } 251 252 public void removeStorage(MtpStorage storage) { 253 mStorageMap.remove(storage.getPath()); 254 } 255 256 private void initDeviceProperties(Context context) { 257 final String devicePropertiesName = "device-properties"; 258 mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE); 259 File databaseFile = context.getDatabasePath(devicePropertiesName); 260 261 if (databaseFile.exists()) { 262 // for backward compatibility - read device properties from sqlite database 263 // and migrate them to shared prefs 264 SQLiteDatabase db = null; 265 Cursor c = null; 266 try { 267 db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null); 268 if (db != null) { 269 c = db.query("properties", new String[] { "_id", "code", "value" }, 270 null, null, null, null, null); 271 if (c != null) { 272 SharedPreferences.Editor e = mDeviceProperties.edit(); 273 while (c.moveToNext()) { 274 String name = c.getString(1); 275 String value = c.getString(2); 276 e.putString(name, value); 277 } 278 e.commit(); 279 } 280 } 281 } catch (Exception e) { 282 Log.e(TAG, "failed to migrate device properties", e); 283 } finally { 284 if (c != null) c.close(); 285 if (db != null) db.close(); 286 } 287 context.deleteDatabase(devicePropertiesName); 288 } 289 } 290 291 // check to see if the path is contained in one of our storage subdirectories 292 // returns true if we have no special subdirectories 293 private boolean inStorageSubDirectory(String path) { 294 if (mSubDirectories == null) return true; 295 if (path == null) return false; 296 297 boolean allowed = false; 298 int pathLength = path.length(); 299 for (int i = 0; i < mSubDirectories.length && !allowed; i++) { 300 String subdir = mSubDirectories[i]; 301 int subdirLength = subdir.length(); 302 if (subdirLength < pathLength && 303 path.charAt(subdirLength) == '/' && 304 path.startsWith(subdir)) { 305 allowed = true; 306 } 307 } 308 return allowed; 309 } 310 311 // check to see if the path matches one of our storage subdirectories 312 // returns true if we have no special subdirectories 313 private boolean isStorageSubDirectory(String path) { 314 if (mSubDirectories == null) return false; 315 for (int i = 0; i < mSubDirectories.length; i++) { 316 if (path.equals(mSubDirectories[i])) { 317 return true; 318 } 319 } 320 return false; 321 } 322 323 // returns true if the path is in the storage root 324 private boolean inStorageRoot(String path) { 325 try { 326 File f = new File(path); 327 String canonical = f.getCanonicalPath(); 328 for (String root: mStorageMap.keySet()) { 329 if (canonical.startsWith(root)) { 330 return true; 331 } 332 } 333 } catch (IOException e) { 334 // ignore 335 } 336 return false; 337 } 338 339 private int beginSendObject(String path, int format, int parent, 340 int storageId, long size, long modified) { 341 // if the path is outside of the storage root, do not allow access 342 if (!inStorageRoot(path)) { 343 Log.e(TAG, "attempt to put file outside of storage area: " + path); 344 return -1; 345 } 346 // if mSubDirectories is not null, do not allow copying files to any other locations 347 if (!inStorageSubDirectory(path)) return -1; 348 349 // make sure the object does not exist 350 if (path != null) { 351 Cursor c = null; 352 try { 353 c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE, 354 new String[] { path }, null, null); 355 if (c != null && c.getCount() > 0) { 356 Log.w(TAG, "file already exists in beginSendObject: " + path); 357 return -1; 358 } 359 } catch (RemoteException e) { 360 Log.e(TAG, "RemoteException in beginSendObject", e); 361 } finally { 362 if (c != null) { 363 c.close(); 364 } 365 } 366 } 367 368 mDatabaseModified = true; 369 ContentValues values = new ContentValues(); 370 values.put(Files.FileColumns.DATA, path); 371 values.put(Files.FileColumns.FORMAT, format); 372 values.put(Files.FileColumns.PARENT, parent); 373 values.put(Files.FileColumns.STORAGE_ID, storageId); 374 values.put(Files.FileColumns.SIZE, size); 375 values.put(Files.FileColumns.DATE_MODIFIED, modified); 376 377 try { 378 Uri uri = mMediaProvider.insert(mObjectsUri, values); 379 if (uri != null) { 380 return Integer.parseInt(uri.getPathSegments().get(2)); 381 } else { 382 return -1; 383 } 384 } catch (RemoteException e) { 385 Log.e(TAG, "RemoteException in beginSendObject", e); 386 return -1; 387 } 388 } 389 390 private void endSendObject(String path, int handle, int format, boolean succeeded) { 391 if (succeeded) { 392 // handle abstract playlists separately 393 // they do not exist in the file system so don't use the media scanner here 394 if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) { 395 // extract name from path 396 String name = path; 397 int lastSlash = name.lastIndexOf('/'); 398 if (lastSlash >= 0) { 399 name = name.substring(lastSlash + 1); 400 } 401 // strip trailing ".pla" from the name 402 if (name.endsWith(".pla")) { 403 name = name.substring(0, name.length() - 4); 404 } 405 406 ContentValues values = new ContentValues(1); 407 values.put(Audio.Playlists.DATA, path); 408 values.put(Audio.Playlists.NAME, name); 409 values.put(Files.FileColumns.FORMAT, format); 410 values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000); 411 values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle); 412 try { 413 Uri uri = mMediaProvider.insert( 414 Audio.Playlists.EXTERNAL_CONTENT_URI, values); 415 } catch (RemoteException e) { 416 Log.e(TAG, "RemoteException in endSendObject", e); 417 } 418 } else { 419 mMediaScanner.scanMtpFile(path, handle, format); 420 } 421 } else { 422 deleteFile(handle); 423 } 424 } 425 426 private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException { 427 String where; 428 String[] whereArgs; 429 430 if (storageID == 0xFFFFFFFF) { 431 // query all stores 432 if (format == 0) { 433 // query all formats 434 if (parent == 0) { 435 // query all objects 436 where = null; 437 whereArgs = null; 438 } else { 439 if (parent == 0xFFFFFFFF) { 440 // all objects in root of store 441 parent = 0; 442 } 443 where = PARENT_WHERE; 444 whereArgs = new String[] { Integer.toString(parent) }; 445 } 446 } else { 447 // query specific format 448 if (parent == 0) { 449 // query all objects 450 where = FORMAT_WHERE; 451 whereArgs = new String[] { Integer.toString(format) }; 452 } else { 453 if (parent == 0xFFFFFFFF) { 454 // all objects in root of store 455 parent = 0; 456 } 457 where = FORMAT_PARENT_WHERE; 458 whereArgs = new String[] { Integer.toString(format), 459 Integer.toString(parent) }; 460 } 461 } 462 } else { 463 // query specific store 464 if (format == 0) { 465 // query all formats 466 if (parent == 0) { 467 // query all objects 468 where = STORAGE_WHERE; 469 whereArgs = new String[] { Integer.toString(storageID) }; 470 } else { 471 if (parent == 0xFFFFFFFF) { 472 // all objects in root of store 473 parent = 0; 474 } 475 where = STORAGE_PARENT_WHERE; 476 whereArgs = new String[] { Integer.toString(storageID), 477 Integer.toString(parent) }; 478 } 479 } else { 480 // query specific format 481 if (parent == 0) { 482 // query all objects 483 where = STORAGE_FORMAT_WHERE; 484 whereArgs = new String[] { Integer.toString(storageID), 485 Integer.toString(format) }; 486 } else { 487 if (parent == 0xFFFFFFFF) { 488 // all objects in root of store 489 parent = 0; 490 } 491 where = STORAGE_FORMAT_PARENT_WHERE; 492 whereArgs = new String[] { Integer.toString(storageID), 493 Integer.toString(format), 494 Integer.toString(parent) }; 495 } 496 } 497 } 498 499 // if we are restricting queries to mSubDirectories, we need to add the restriction 500 // onto our "where" arguments 501 if (mSubDirectoriesWhere != null) { 502 if (where == null) { 503 where = mSubDirectoriesWhere; 504 whereArgs = mSubDirectoriesWhereArgs; 505 } else { 506 where = where + " AND " + mSubDirectoriesWhere; 507 508 // create new array to hold whereArgs and mSubDirectoriesWhereArgs 509 String[] newWhereArgs = 510 new String[whereArgs.length + mSubDirectoriesWhereArgs.length]; 511 int i, j; 512 for (i = 0; i < whereArgs.length; i++) { 513 newWhereArgs[i] = whereArgs[i]; 514 } 515 for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) { 516 newWhereArgs[i] = mSubDirectoriesWhereArgs[j]; 517 } 518 whereArgs = newWhereArgs; 519 } 520 } 521 522 return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where, 523 whereArgs, null, null); 524 } 525 526 private int[] getObjectList(int storageID, int format, int parent) { 527 Cursor c = null; 528 try { 529 c = createObjectQuery(storageID, format, parent); 530 if (c == null) { 531 return null; 532 } 533 int count = c.getCount(); 534 if (count > 0) { 535 int[] result = new int[count]; 536 for (int i = 0; i < count; i++) { 537 c.moveToNext(); 538 result[i] = c.getInt(0); 539 } 540 return result; 541 } 542 } catch (RemoteException e) { 543 Log.e(TAG, "RemoteException in getObjectList", e); 544 } finally { 545 if (c != null) { 546 c.close(); 547 } 548 } 549 return null; 550 } 551 552 private int getNumObjects(int storageID, int format, int parent) { 553 Cursor c = null; 554 try { 555 c = createObjectQuery(storageID, format, parent); 556 if (c != null) { 557 return c.getCount(); 558 } 559 } catch (RemoteException e) { 560 Log.e(TAG, "RemoteException in getNumObjects", e); 561 } finally { 562 if (c != null) { 563 c.close(); 564 } 565 } 566 return -1; 567 } 568 569 private int[] getSupportedPlaybackFormats() { 570 return new int[] { 571 // allow transfering arbitrary files 572 MtpConstants.FORMAT_UNDEFINED, 573 574 MtpConstants.FORMAT_ASSOCIATION, 575 MtpConstants.FORMAT_TEXT, 576 MtpConstants.FORMAT_HTML, 577 MtpConstants.FORMAT_WAV, 578 MtpConstants.FORMAT_MP3, 579 MtpConstants.FORMAT_MPEG, 580 MtpConstants.FORMAT_EXIF_JPEG, 581 MtpConstants.FORMAT_TIFF_EP, 582 MtpConstants.FORMAT_BMP, 583 MtpConstants.FORMAT_GIF, 584 MtpConstants.FORMAT_JFIF, 585 MtpConstants.FORMAT_PNG, 586 MtpConstants.FORMAT_TIFF, 587 MtpConstants.FORMAT_WMA, 588 MtpConstants.FORMAT_OGG, 589 MtpConstants.FORMAT_AAC, 590 MtpConstants.FORMAT_MP4_CONTAINER, 591 MtpConstants.FORMAT_MP2, 592 MtpConstants.FORMAT_3GP_CONTAINER, 593 MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, 594 MtpConstants.FORMAT_WPL_PLAYLIST, 595 MtpConstants.FORMAT_M3U_PLAYLIST, 596 MtpConstants.FORMAT_PLS_PLAYLIST, 597 MtpConstants.FORMAT_XML_DOCUMENT, 598 MtpConstants.FORMAT_FLAC, 599 MtpConstants.FORMAT_DNG, 600 MtpConstants.FORMAT_HEIF, 601 }; 602 } 603 604 private int[] getSupportedCaptureFormats() { 605 // no capture formats yet 606 return null; 607 } 608 609 static final int[] FILE_PROPERTIES = { 610 // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES 611 // and IMAGE_PROPERTIES below 612 MtpConstants.PROPERTY_STORAGE_ID, 613 MtpConstants.PROPERTY_OBJECT_FORMAT, 614 MtpConstants.PROPERTY_PROTECTION_STATUS, 615 MtpConstants.PROPERTY_OBJECT_SIZE, 616 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 617 MtpConstants.PROPERTY_DATE_MODIFIED, 618 MtpConstants.PROPERTY_PARENT_OBJECT, 619 MtpConstants.PROPERTY_PERSISTENT_UID, 620 MtpConstants.PROPERTY_NAME, 621 MtpConstants.PROPERTY_DISPLAY_NAME, 622 MtpConstants.PROPERTY_DATE_ADDED, 623 }; 624 625 static final int[] AUDIO_PROPERTIES = { 626 // NOTE must match FILE_PROPERTIES above 627 MtpConstants.PROPERTY_STORAGE_ID, 628 MtpConstants.PROPERTY_OBJECT_FORMAT, 629 MtpConstants.PROPERTY_PROTECTION_STATUS, 630 MtpConstants.PROPERTY_OBJECT_SIZE, 631 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 632 MtpConstants.PROPERTY_DATE_MODIFIED, 633 MtpConstants.PROPERTY_PARENT_OBJECT, 634 MtpConstants.PROPERTY_PERSISTENT_UID, 635 MtpConstants.PROPERTY_NAME, 636 MtpConstants.PROPERTY_DISPLAY_NAME, 637 MtpConstants.PROPERTY_DATE_ADDED, 638 639 // audio specific properties 640 MtpConstants.PROPERTY_ARTIST, 641 MtpConstants.PROPERTY_ALBUM_NAME, 642 MtpConstants.PROPERTY_ALBUM_ARTIST, 643 MtpConstants.PROPERTY_TRACK, 644 MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, 645 MtpConstants.PROPERTY_DURATION, 646 MtpConstants.PROPERTY_GENRE, 647 MtpConstants.PROPERTY_COMPOSER, 648 MtpConstants.PROPERTY_AUDIO_WAVE_CODEC, 649 MtpConstants.PROPERTY_BITRATE_TYPE, 650 MtpConstants.PROPERTY_AUDIO_BITRATE, 651 MtpConstants.PROPERTY_NUMBER_OF_CHANNELS, 652 MtpConstants.PROPERTY_SAMPLE_RATE, 653 }; 654 655 static final int[] VIDEO_PROPERTIES = { 656 // NOTE must match FILE_PROPERTIES above 657 MtpConstants.PROPERTY_STORAGE_ID, 658 MtpConstants.PROPERTY_OBJECT_FORMAT, 659 MtpConstants.PROPERTY_PROTECTION_STATUS, 660 MtpConstants.PROPERTY_OBJECT_SIZE, 661 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 662 MtpConstants.PROPERTY_DATE_MODIFIED, 663 MtpConstants.PROPERTY_PARENT_OBJECT, 664 MtpConstants.PROPERTY_PERSISTENT_UID, 665 MtpConstants.PROPERTY_NAME, 666 MtpConstants.PROPERTY_DISPLAY_NAME, 667 MtpConstants.PROPERTY_DATE_ADDED, 668 669 // video specific properties 670 MtpConstants.PROPERTY_ARTIST, 671 MtpConstants.PROPERTY_ALBUM_NAME, 672 MtpConstants.PROPERTY_DURATION, 673 MtpConstants.PROPERTY_DESCRIPTION, 674 }; 675 676 static final int[] IMAGE_PROPERTIES = { 677 // NOTE must match FILE_PROPERTIES above 678 MtpConstants.PROPERTY_STORAGE_ID, 679 MtpConstants.PROPERTY_OBJECT_FORMAT, 680 MtpConstants.PROPERTY_PROTECTION_STATUS, 681 MtpConstants.PROPERTY_OBJECT_SIZE, 682 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 683 MtpConstants.PROPERTY_DATE_MODIFIED, 684 MtpConstants.PROPERTY_PARENT_OBJECT, 685 MtpConstants.PROPERTY_PERSISTENT_UID, 686 MtpConstants.PROPERTY_NAME, 687 MtpConstants.PROPERTY_DISPLAY_NAME, 688 MtpConstants.PROPERTY_DATE_ADDED, 689 690 // image specific properties 691 MtpConstants.PROPERTY_DESCRIPTION, 692 }; 693 694 private int[] getSupportedObjectProperties(int format) { 695 switch (format) { 696 case MtpConstants.FORMAT_MP3: 697 case MtpConstants.FORMAT_WAV: 698 case MtpConstants.FORMAT_WMA: 699 case MtpConstants.FORMAT_OGG: 700 case MtpConstants.FORMAT_AAC: 701 return AUDIO_PROPERTIES; 702 case MtpConstants.FORMAT_MPEG: 703 case MtpConstants.FORMAT_3GP_CONTAINER: 704 case MtpConstants.FORMAT_WMV: 705 return VIDEO_PROPERTIES; 706 case MtpConstants.FORMAT_EXIF_JPEG: 707 case MtpConstants.FORMAT_GIF: 708 case MtpConstants.FORMAT_PNG: 709 case MtpConstants.FORMAT_BMP: 710 case MtpConstants.FORMAT_DNG: 711 case MtpConstants.FORMAT_HEIF: 712 return IMAGE_PROPERTIES; 713 default: 714 return FILE_PROPERTIES; 715 } 716 } 717 718 private int[] getSupportedDeviceProperties() { 719 return new int[] { 720 MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, 721 MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, 722 MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE, 723 MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL, 724 MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE, 725 }; 726 } 727 728 private MtpPropertyList getObjectPropertyList(int handle, int format, int property, 729 int groupCode, int depth) { 730 // FIXME - implement group support 731 if (groupCode != 0) { 732 return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED); 733 } 734 735 MtpPropertyGroup propertyGroup; 736 if (property == 0xffffffff) { 737 if (format == 0 && handle != 0 && handle != 0xffffffff) { 738 // return properties based on the object's format 739 format = getObjectFormat(handle); 740 } 741 propertyGroup = mPropertyGroupsByFormat.get(format); 742 if (propertyGroup == null) { 743 int[] propertyList = getSupportedObjectProperties(format); 744 propertyGroup = new MtpPropertyGroup(this, mMediaProvider, 745 mVolumeName, propertyList); 746 mPropertyGroupsByFormat.put(format, propertyGroup); 747 } 748 } else { 749 propertyGroup = mPropertyGroupsByProperty.get(property); 750 if (propertyGroup == null) { 751 final int[] propertyList = new int[] { property }; 752 propertyGroup = new MtpPropertyGroup( 753 this, mMediaProvider, mVolumeName, propertyList); 754 mPropertyGroupsByProperty.put(property, propertyGroup); 755 } 756 } 757 758 return propertyGroup.getPropertyList(handle, format, depth); 759 } 760 761 private int renameFile(int handle, String newName) { 762 Cursor c = null; 763 764 // first compute current path 765 String path = null; 766 String[] whereArgs = new String[] { Integer.toString(handle) }; 767 try { 768 c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, 769 whereArgs, null, null); 770 if (c != null && c.moveToNext()) { 771 path = c.getString(1); 772 } 773 } catch (RemoteException e) { 774 Log.e(TAG, "RemoteException in getObjectFilePath", e); 775 return MtpConstants.RESPONSE_GENERAL_ERROR; 776 } finally { 777 if (c != null) { 778 c.close(); 779 } 780 } 781 if (path == null) { 782 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 783 } 784 785 // do not allow renaming any of the special subdirectories 786 if (isStorageSubDirectory(path)) { 787 return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; 788 } 789 790 // now rename the file. make sure this succeeds before updating database 791 File oldFile = new File(path); 792 int lastSlash = path.lastIndexOf('/'); 793 if (lastSlash <= 1) { 794 return MtpConstants.RESPONSE_GENERAL_ERROR; 795 } 796 String newPath = path.substring(0, lastSlash + 1) + newName; 797 File newFile = new File(newPath); 798 boolean success = oldFile.renameTo(newFile); 799 if (!success) { 800 Log.w(TAG, "renaming "+ path + " to " + newPath + " failed"); 801 return MtpConstants.RESPONSE_GENERAL_ERROR; 802 } 803 804 // finally update database 805 ContentValues values = new ContentValues(); 806 values.put(Files.FileColumns.DATA, newPath); 807 int updated = 0; 808 try { 809 // note - we are relying on a special case in MediaProvider.update() to update 810 // the paths for all children in the case where this is a directory. 811 updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs); 812 } catch (RemoteException e) { 813 Log.e(TAG, "RemoteException in mMediaProvider.update", e); 814 } 815 if (updated == 0) { 816 Log.e(TAG, "Unable to update path for " + path + " to " + newPath); 817 // this shouldn't happen, but if it does we need to rename the file to its original name 818 newFile.renameTo(oldFile); 819 return MtpConstants.RESPONSE_GENERAL_ERROR; 820 } 821 822 // check if nomedia status changed 823 if (newFile.isDirectory()) { 824 // for directories, check if renamed from something hidden to something non-hidden 825 if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) { 826 // directory was unhidden 827 try { 828 mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath, null); 829 } catch (RemoteException e) { 830 Log.e(TAG, "failed to unhide/rescan for " + newPath); 831 } 832 } 833 } else { 834 // for files, check if renamed from .nomedia to something else 835 if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia") 836 && !newPath.toLowerCase(Locale.US).equals(".nomedia")) { 837 try { 838 mMediaProvider.call(MediaStore.UNHIDE_CALL, oldFile.getParent(), null); 839 } catch (RemoteException e) { 840 Log.e(TAG, "failed to unhide/rescan for " + newPath); 841 } 842 } 843 } 844 845 return MtpConstants.RESPONSE_OK; 846 } 847 848 private int setObjectProperty(int handle, int property, 849 long intValue, String stringValue) { 850 switch (property) { 851 case MtpConstants.PROPERTY_OBJECT_FILE_NAME: 852 return renameFile(handle, stringValue); 853 854 default: 855 return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED; 856 } 857 } 858 859 private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) { 860 switch (property) { 861 case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: 862 case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: 863 // writable string properties kept in shared preferences 864 String value = mDeviceProperties.getString(Integer.toString(property), ""); 865 int length = value.length(); 866 if (length > 255) { 867 length = 255; 868 } 869 value.getChars(0, length, outStringValue, 0); 870 outStringValue[length] = 0; 871 return MtpConstants.RESPONSE_OK; 872 873 case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE: 874 // use screen size as max image size 875 Display display = ((WindowManager)mContext.getSystemService( 876 Context.WINDOW_SERVICE)).getDefaultDisplay(); 877 int width = display.getMaximumSizeDimension(); 878 int height = display.getMaximumSizeDimension(); 879 String imageSize = Integer.toString(width) + "x" + Integer.toString(height); 880 imageSize.getChars(0, imageSize.length(), outStringValue, 0); 881 outStringValue[imageSize.length()] = 0; 882 return MtpConstants.RESPONSE_OK; 883 884 case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE: 885 outIntValue[0] = mDeviceType; 886 return MtpConstants.RESPONSE_OK; 887 888 // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code 889 890 default: 891 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; 892 } 893 } 894 895 private int setDeviceProperty(int property, long intValue, String stringValue) { 896 switch (property) { 897 case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: 898 case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: 899 // writable string properties kept in shared prefs 900 SharedPreferences.Editor e = mDeviceProperties.edit(); 901 e.putString(Integer.toString(property), stringValue); 902 return (e.commit() ? MtpConstants.RESPONSE_OK 903 : MtpConstants.RESPONSE_GENERAL_ERROR); 904 } 905 906 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; 907 } 908 909 private boolean getObjectInfo(int handle, int[] outStorageFormatParent, 910 char[] outName, long[] outCreatedModified) { 911 Cursor c = null; 912 try { 913 c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION, 914 ID_WHERE, new String[] { Integer.toString(handle) }, null, null); 915 if (c != null && c.moveToNext()) { 916 outStorageFormatParent[0] = c.getInt(1); 917 outStorageFormatParent[1] = c.getInt(2); 918 outStorageFormatParent[2] = c.getInt(3); 919 920 // extract name from path 921 String path = c.getString(4); 922 int lastSlash = path.lastIndexOf('/'); 923 int start = (lastSlash >= 0 ? lastSlash + 1 : 0); 924 int end = path.length(); 925 if (end - start > 255) { 926 end = start + 255; 927 } 928 path.getChars(start, end, outName, 0); 929 outName[end - start] = 0; 930 931 outCreatedModified[0] = c.getLong(5); 932 outCreatedModified[1] = c.getLong(6); 933 // use modification date as creation date if date added is not set 934 if (outCreatedModified[0] == 0) { 935 outCreatedModified[0] = outCreatedModified[1]; 936 } 937 return true; 938 } 939 } catch (RemoteException e) { 940 Log.e(TAG, "RemoteException in getObjectInfo", e); 941 } finally { 942 if (c != null) { 943 c.close(); 944 } 945 } 946 return false; 947 } 948 949 private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) { 950 if (handle == 0) { 951 // special case root directory 952 mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0); 953 outFilePath[mMediaStoragePath.length()] = 0; 954 outFileLengthFormat[0] = 0; 955 outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION; 956 return MtpConstants.RESPONSE_OK; 957 } 958 Cursor c = null; 959 try { 960 c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION, 961 ID_WHERE, new String[] { Integer.toString(handle) }, null, null); 962 if (c != null && c.moveToNext()) { 963 String path = c.getString(1); 964 path.getChars(0, path.length(), outFilePath, 0); 965 outFilePath[path.length()] = 0; 966 // File transfers from device to host will likely fail if the size is incorrect. 967 // So to be safe, use the actual file size here. 968 outFileLengthFormat[0] = new File(path).length(); 969 outFileLengthFormat[1] = c.getLong(2); 970 return MtpConstants.RESPONSE_OK; 971 } else { 972 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 973 } 974 } catch (RemoteException e) { 975 Log.e(TAG, "RemoteException in getObjectFilePath", e); 976 return MtpConstants.RESPONSE_GENERAL_ERROR; 977 } finally { 978 if (c != null) { 979 c.close(); 980 } 981 } 982 } 983 984 private int getObjectFormat(int handle) { 985 Cursor c = null; 986 try { 987 c = mMediaProvider.query(mObjectsUri, FORMAT_PROJECTION, 988 ID_WHERE, new String[] { Integer.toString(handle) }, null, null); 989 if (c != null && c.moveToNext()) { 990 return c.getInt(1); 991 } else { 992 return -1; 993 } 994 } catch (RemoteException e) { 995 Log.e(TAG, "RemoteException in getObjectFilePath", e); 996 return -1; 997 } finally { 998 if (c != null) { 999 c.close(); 1000 } 1001 } 1002 } 1003 1004 private int deleteFile(int handle) { 1005 mDatabaseModified = true; 1006 String path = null; 1007 int format = 0; 1008 1009 Cursor c = null; 1010 try { 1011 c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION, 1012 ID_WHERE, new String[] { Integer.toString(handle) }, null, null); 1013 if (c != null && c.moveToNext()) { 1014 // don't convert to media path here, since we will be matching 1015 // against paths in the database matching /data/media 1016 path = c.getString(1); 1017 format = c.getInt(2); 1018 } else { 1019 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 1020 } 1021 1022 if (path == null || format == 0) { 1023 return MtpConstants.RESPONSE_GENERAL_ERROR; 1024 } 1025 1026 // do not allow deleting any of the special subdirectories 1027 if (isStorageSubDirectory(path)) { 1028 return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED; 1029 } 1030 1031 if (format == MtpConstants.FORMAT_ASSOCIATION) { 1032 // recursive case - delete all children first 1033 Uri uri = Files.getMtpObjectsUri(mVolumeName); 1034 int count = mMediaProvider.delete(uri, 1035 // the 'like' makes it use the index, the 'lower()' makes it correct 1036 // when the path contains sqlite wildcard characters 1037 "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)", 1038 new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"}); 1039 } 1040 1041 Uri uri = Files.getMtpObjectsUri(mVolumeName, handle); 1042 if (mMediaProvider.delete(uri, null, null) > 0) { 1043 if (format != MtpConstants.FORMAT_ASSOCIATION 1044 && path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 1045 try { 1046 String parentPath = path.substring(0, path.lastIndexOf("/")); 1047 mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null); 1048 } catch (RemoteException e) { 1049 Log.e(TAG, "failed to unhide/rescan for " + path); 1050 } 1051 } 1052 return MtpConstants.RESPONSE_OK; 1053 } else { 1054 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 1055 } 1056 } catch (RemoteException e) { 1057 Log.e(TAG, "RemoteException in deleteFile", e); 1058 return MtpConstants.RESPONSE_GENERAL_ERROR; 1059 } finally { 1060 if (c != null) { 1061 c.close(); 1062 } 1063 } 1064 } 1065 1066 private int[] getObjectReferences(int handle) { 1067 Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); 1068 Cursor c = null; 1069 try { 1070 c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null, null); 1071 if (c == null) { 1072 return null; 1073 } 1074 int count = c.getCount(); 1075 if (count > 0) { 1076 int[] result = new int[count]; 1077 for (int i = 0; i < count; i++) { 1078 c.moveToNext(); 1079 result[i] = c.getInt(0); 1080 } 1081 return result; 1082 } 1083 } catch (RemoteException e) { 1084 Log.e(TAG, "RemoteException in getObjectList", e); 1085 } finally { 1086 if (c != null) { 1087 c.close(); 1088 } 1089 } 1090 return null; 1091 } 1092 1093 private int setObjectReferences(int handle, int[] references) { 1094 mDatabaseModified = true; 1095 Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); 1096 int count = references.length; 1097 ContentValues[] valuesList = new ContentValues[count]; 1098 for (int i = 0; i < count; i++) { 1099 ContentValues values = new ContentValues(); 1100 values.put(Files.FileColumns._ID, references[i]); 1101 valuesList[i] = values; 1102 } 1103 try { 1104 if (mMediaProvider.bulkInsert(uri, valuesList) > 0) { 1105 return MtpConstants.RESPONSE_OK; 1106 } 1107 } catch (RemoteException e) { 1108 Log.e(TAG, "RemoteException in setObjectReferences", e); 1109 } 1110 return MtpConstants.RESPONSE_GENERAL_ERROR; 1111 } 1112 1113 private void sessionStarted() { 1114 mDatabaseModified = false; 1115 } 1116 1117 private void sessionEnded() { 1118 if (mDatabaseModified) { 1119 mUserContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END)); 1120 mDatabaseModified = false; 1121 } 1122 } 1123 1124 // used by the JNI code 1125 private long mNativeContext; 1126 1127 private native final void native_setup(); 1128 private native final void native_finalize(); 1129 } 1130