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