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