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