MtpDatabase.java revision 2b5f9ad1eaf0d6daaca5cf3761434a09309902bb
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.media; 18 19import android.content.Context; 20import android.content.ContentValues; 21import android.content.IContentProvider; 22import android.content.Intent; 23import android.database.Cursor; 24import android.database.sqlite.SQLiteDatabase; 25import android.net.Uri; 26import android.os.Environment; 27import android.os.RemoteException; 28import android.provider.MediaStore.Audio; 29import android.provider.MediaStore.Files; 30import android.provider.MediaStore.Images; 31import android.provider.MediaStore.MediaColumns; 32import android.provider.Mtp; 33import android.util.Log; 34 35import java.io.File; 36 37/** 38 * {@hide} 39 */ 40public class MtpDatabase { 41 42 private static final String TAG = "MtpDatabase"; 43 44 private final Context mContext; 45 private final IContentProvider mMediaProvider; 46 private final String mVolumeName; 47 private final Uri mObjectsUri; 48 private final String mMediaStoragePath; 49 private final String mExternalStoragePath; 50 51 // true if the database has been modified in the current MTP session 52 private boolean mDatabaseModified; 53 54 // database for writable MTP device properties 55 private SQLiteDatabase mDevicePropDb; 56 private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1; 57 58 // FIXME - this should be passed in via the constructor 59 private final int mStorageID = 0x00010001; 60 61 private static final String[] ID_PROJECTION = new String[] { 62 Files.FileColumns._ID, // 0 63 }; 64 private static final String[] PATH_PROJECTION = new String[] { 65 Files.FileColumns._ID, // 0 66 Files.FileColumns.DATA, // 1 67 }; 68 private static final String[] PATH_SIZE_PROJECTION = new String[] { 69 Files.FileColumns._ID, // 0 70 Files.FileColumns.DATA, // 1 71 Files.FileColumns.SIZE, // 2 72 }; 73 private static final String[] OBJECT_INFO_PROJECTION = new String[] { 74 Files.FileColumns._ID, // 0 75 Files.FileColumns.DATA, // 1 76 Files.FileColumns.FORMAT, // 2 77 Files.FileColumns.PARENT, // 3 78 Files.FileColumns.SIZE, // 4 79 Files.FileColumns.DATE_MODIFIED, // 5 80 }; 81 private static final String ID_WHERE = Files.FileColumns._ID + "=?"; 82 private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?"; 83 private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " 84 + Files.FileColumns.FORMAT + "=?"; 85 86 private static final String[] DEVICE_PROPERTY_PROJECTION = new String[] { "_id", "value" }; 87 private static final String DEVICE_PROPERTY_WHERE = "code=?"; 88 89 private final MediaScanner mMediaScanner; 90 91 static { 92 System.loadLibrary("media_jni"); 93 } 94 95 public MtpDatabase(Context context, String volumeName, String storagePath) { 96 native_setup(); 97 98 mContext = context; 99 mMediaProvider = context.getContentResolver().acquireProvider("media"); 100 mVolumeName = volumeName; 101 mMediaStoragePath = storagePath; 102 mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath(); 103 mObjectsUri = Files.getMtpObjectsUri(volumeName); 104 mMediaScanner = new MediaScanner(context); 105 openDevicePropertiesDatabase(context); 106 } 107 108 @Override 109 protected void finalize() throws Throwable { 110 try { 111 native_finalize(); 112 } finally { 113 super.finalize(); 114 } 115 } 116 117 private String externalToMediaPath(String path) { 118 // convert external storage path to media path 119 if (path != null && mMediaStoragePath != null 120 && mExternalStoragePath != null 121 && path.startsWith(mExternalStoragePath)) { 122 path = mMediaStoragePath + path.substring(mExternalStoragePath.length()); 123 } 124 return path; 125 } 126 127 private void openDevicePropertiesDatabase(Context context) { 128 mDevicePropDb = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null); 129 int version = mDevicePropDb.getVersion(); 130 131 // initialize if necessary 132 if (version != DEVICE_PROPERTIES_DATABASE_VERSION) { 133 mDevicePropDb.execSQL("CREATE TABLE properties (" + 134 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 135 "code INTEGER UNIQUE ON CONFLICT REPLACE," + 136 "value TEXT" + 137 ");"); 138 mDevicePropDb.execSQL("CREATE INDEX property_index ON properties (code);"); 139 mDevicePropDb.setVersion(DEVICE_PROPERTIES_DATABASE_VERSION); 140 } 141 } 142 143 private int beginSendObject(String path, int format, int parent, 144 int storage, long size, long modified) { 145 mDatabaseModified = true; 146 ContentValues values = new ContentValues(); 147 values.put(Files.FileColumns.DATA, path); 148 values.put(Files.FileColumns.FORMAT, format); 149 values.put(Files.FileColumns.PARENT, parent); 150 // storage is ignored for now 151 values.put(Files.FileColumns.SIZE, size); 152 values.put(Files.FileColumns.DATE_MODIFIED, modified); 153 154 try { 155 Uri uri = mMediaProvider.insert(mObjectsUri, values); 156 if (uri != null) { 157 return Integer.parseInt(uri.getPathSegments().get(2)); 158 } else { 159 return -1; 160 } 161 } catch (RemoteException e) { 162 Log.e(TAG, "RemoteException in beginSendObject", e); 163 return -1; 164 } 165 } 166 167 private void endSendObject(String path, int handle, int format, boolean succeeded) { 168 if (succeeded) { 169 // handle abstract playlists separately 170 // they do not exist in the file system so don't use the media scanner here 171 if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) { 172 // Strip Windows Media Player file extension 173 if (path.endsWith(".pla")) { 174 path = path.substring(0, path.length() - 4); 175 } 176 177 // extract name from path 178 String name = path; 179 int lastSlash = name.lastIndexOf('/'); 180 if (lastSlash >= 0) { 181 name = name.substring(lastSlash + 1); 182 } 183 184 ContentValues values = new ContentValues(1); 185 values.put(Audio.Playlists.DATA, path); 186 values.put(Audio.Playlists.NAME, name); 187 values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle); 188 try { 189 Uri uri = mMediaProvider.insert(Audio.Playlists.EXTERNAL_CONTENT_URI, values); 190 } catch (RemoteException e) { 191 Log.e(TAG, "RemoteException in endSendObject", e); 192 } 193 } else { 194 mMediaScanner.scanMtpFile(path, mVolumeName, handle, format); 195 } 196 } else { 197 deleteFile(handle); 198 } 199 } 200 201 private int[] getObjectList(int storageID, int format, int parent) { 202 // we can ignore storageID until we support multiple storages 203 Log.d(TAG, "getObjectList parent: " + parent); 204 Cursor c = null; 205 try { 206 if (format != 0) { 207 c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, 208 PARENT_FORMAT_WHERE, 209 new String[] { Integer.toString(parent), Integer.toString(format) }, 210 null); 211 } else { 212 c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, 213 PARENT_WHERE, new String[] { Integer.toString(parent) }, null); 214 } 215 if (c == null) { 216 Log.d(TAG, "null cursor"); 217 return null; 218 } 219 int count = c.getCount(); 220 if (count > 0) { 221 int[] result = new int[count]; 222 for (int i = 0; i < count; i++) { 223 c.moveToNext(); 224 result[i] = c.getInt(0); 225 } 226 Log.d(TAG, "returning " + result); 227 return result; 228 } 229 } catch (RemoteException e) { 230 Log.e(TAG, "RemoteException in getObjectList", e); 231 } finally { 232 if (c != null) { 233 c.close(); 234 } 235 } 236 return null; 237 } 238 239 private int getNumObjects(int storageID, int format, int parent) { 240 // we can ignore storageID until we support multiple storages 241 Log.d(TAG, "getObjectList parent: " + parent); 242 Cursor c = null; 243 try { 244 if (format != 0) { 245 c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, 246 PARENT_FORMAT_WHERE, 247 new String[] { Integer.toString(parent), Integer.toString(format) }, 248 null); 249 } else { 250 c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, 251 PARENT_WHERE, new String[] { Integer.toString(parent) }, null); 252 } 253 if (c != null) { 254 return c.getCount(); 255 } 256 } catch (RemoteException e) { 257 Log.e(TAG, "RemoteException in getNumObjects", e); 258 } finally { 259 if (c != null) { 260 c.close(); 261 } 262 } 263 return -1; 264 } 265 266 private int[] getSupportedPlaybackFormats() { 267 return new int[] { 268 // allow transfering arbitrary files 269 MtpConstants.FORMAT_UNDEFINED, 270 271 MtpConstants.FORMAT_ASSOCIATION, 272 MtpConstants.FORMAT_TEXT, 273 MtpConstants.FORMAT_HTML, 274 MtpConstants.FORMAT_WAV, 275 MtpConstants.FORMAT_MP3, 276 MtpConstants.FORMAT_MPEG, 277 MtpConstants.FORMAT_EXIF_JPEG, 278 MtpConstants.FORMAT_TIFF_EP, 279 MtpConstants.FORMAT_GIF, 280 MtpConstants.FORMAT_JFIF, 281 MtpConstants.FORMAT_PNG, 282 MtpConstants.FORMAT_TIFF, 283 MtpConstants.FORMAT_WMA, 284 MtpConstants.FORMAT_OGG, 285 MtpConstants.FORMAT_AAC, 286 MtpConstants.FORMAT_MP4_CONTAINER, 287 MtpConstants.FORMAT_MP2, 288 MtpConstants.FORMAT_3GP_CONTAINER, 289 MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST, 290 MtpConstants.FORMAT_WPL_PLAYLIST, 291 MtpConstants.FORMAT_M3U_PLAYLIST, 292 MtpConstants.FORMAT_PLS_PLAYLIST, 293 MtpConstants.FORMAT_XML_DOCUMENT, 294 }; 295 } 296 297 private int[] getSupportedCaptureFormats() { 298 // no capture formats yet 299 return null; 300 } 301 302 static final int[] FILE_PROPERTIES = { 303 // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES 304 // and IMAGE_PROPERTIES below 305 MtpConstants.PROPERTY_STORAGE_ID, 306 MtpConstants.PROPERTY_OBJECT_FORMAT, 307 MtpConstants.PROPERTY_PROTECTION_STATUS, 308 MtpConstants.PROPERTY_OBJECT_SIZE, 309 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 310 MtpConstants.PROPERTY_DATE_MODIFIED, 311 MtpConstants.PROPERTY_PARENT_OBJECT, 312 MtpConstants.PROPERTY_PERSISTENT_UID, 313 MtpConstants.PROPERTY_NAME, 314 MtpConstants.PROPERTY_DATE_ADDED, 315 }; 316 317 static final int[] AUDIO_PROPERTIES = { 318 // NOTE must match FILE_PROPERTIES above 319 MtpConstants.PROPERTY_STORAGE_ID, 320 MtpConstants.PROPERTY_OBJECT_FORMAT, 321 MtpConstants.PROPERTY_PROTECTION_STATUS, 322 MtpConstants.PROPERTY_OBJECT_SIZE, 323 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 324 MtpConstants.PROPERTY_DATE_MODIFIED, 325 MtpConstants.PROPERTY_PARENT_OBJECT, 326 MtpConstants.PROPERTY_PERSISTENT_UID, 327 MtpConstants.PROPERTY_NAME, 328 MtpConstants.PROPERTY_DISPLAY_NAME, 329 MtpConstants.PROPERTY_DATE_ADDED, 330 331 // audio specific properties 332 MtpConstants.PROPERTY_ARTIST, 333 MtpConstants.PROPERTY_ALBUM_NAME, 334 MtpConstants.PROPERTY_ALBUM_ARTIST, 335 MtpConstants.PROPERTY_TRACK, 336 MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE, 337 MtpConstants.PROPERTY_DURATION, 338 MtpConstants.PROPERTY_GENRE, 339 MtpConstants.PROPERTY_COMPOSER, 340 }; 341 342 static final int[] VIDEO_PROPERTIES = { 343 // NOTE must match FILE_PROPERTIES above 344 MtpConstants.PROPERTY_STORAGE_ID, 345 MtpConstants.PROPERTY_OBJECT_FORMAT, 346 MtpConstants.PROPERTY_PROTECTION_STATUS, 347 MtpConstants.PROPERTY_OBJECT_SIZE, 348 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 349 MtpConstants.PROPERTY_DATE_MODIFIED, 350 MtpConstants.PROPERTY_PARENT_OBJECT, 351 MtpConstants.PROPERTY_PERSISTENT_UID, 352 MtpConstants.PROPERTY_NAME, 353 MtpConstants.PROPERTY_DISPLAY_NAME, 354 MtpConstants.PROPERTY_DATE_ADDED, 355 356 // video specific properties 357 MtpConstants.PROPERTY_ARTIST, 358 MtpConstants.PROPERTY_ALBUM_NAME, 359 MtpConstants.PROPERTY_DURATION, 360 MtpConstants.PROPERTY_DESCRIPTION, 361 }; 362 363 static final int[] IMAGE_PROPERTIES = { 364 // NOTE must match FILE_PROPERTIES above 365 MtpConstants.PROPERTY_STORAGE_ID, 366 MtpConstants.PROPERTY_OBJECT_FORMAT, 367 MtpConstants.PROPERTY_PROTECTION_STATUS, 368 MtpConstants.PROPERTY_OBJECT_SIZE, 369 MtpConstants.PROPERTY_OBJECT_FILE_NAME, 370 MtpConstants.PROPERTY_DATE_MODIFIED, 371 MtpConstants.PROPERTY_PARENT_OBJECT, 372 MtpConstants.PROPERTY_PERSISTENT_UID, 373 MtpConstants.PROPERTY_NAME, 374 MtpConstants.PROPERTY_DISPLAY_NAME, 375 MtpConstants.PROPERTY_DATE_ADDED, 376 377 // image specific properties 378 MtpConstants.PROPERTY_DESCRIPTION, 379 }; 380 381 private int[] getSupportedObjectProperties(int format) { 382 switch (format) { 383 case MtpConstants.FORMAT_MP3: 384 case MtpConstants.FORMAT_WAV: 385 case MtpConstants.FORMAT_WMA: 386 case MtpConstants.FORMAT_OGG: 387 case MtpConstants.FORMAT_AAC: 388 return AUDIO_PROPERTIES; 389 case MtpConstants.FORMAT_MPEG: 390 case MtpConstants.FORMAT_3GP_CONTAINER: 391 case MtpConstants.FORMAT_WMV: 392 return VIDEO_PROPERTIES; 393 case MtpConstants.FORMAT_EXIF_JPEG: 394 case MtpConstants.FORMAT_GIF: 395 case MtpConstants.FORMAT_PNG: 396 case MtpConstants.FORMAT_BMP: 397 return IMAGE_PROPERTIES; 398 default: 399 return FILE_PROPERTIES; 400 } 401 } 402 403 private int[] getSupportedDeviceProperties() { 404 return new int[] { 405 MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER, 406 MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME, 407 }; 408 } 409 410 private String queryString(int id, String column) { 411 Cursor c = null; 412 try { 413 // for now we are only reading properties from the "objects" table 414 c = mMediaProvider.query(mObjectsUri, 415 new String [] { Files.FileColumns._ID, column }, 416 ID_WHERE, new String[] { Integer.toString(id) }, null); 417 if (c != null && c.moveToNext()) { 418 return c.getString(1); 419 } else { 420 return ""; 421 } 422 } catch (Exception e) { 423 return null; 424 } finally { 425 if (c != null) { 426 c.close(); 427 } 428 } 429 } 430 431 private String queryGenre(int id) { 432 Cursor c = null; 433 try { 434 Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id); 435 c = mMediaProvider.query(uri, 436 new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME }, 437 null, null, null); 438 if (c != null && c.moveToNext()) { 439 return c.getString(1); 440 } else { 441 return ""; 442 } 443 } catch (Exception e) { 444 Log.e(TAG, "queryGenre exception", e); 445 return null; 446 } finally { 447 if (c != null) { 448 c.close(); 449 } 450 } 451 } 452 453 private boolean queryInt(int id, String column, long[] outValue) { 454 Cursor c = null; 455 try { 456 // for now we are only reading properties from the "objects" table 457 c = mMediaProvider.query(mObjectsUri, 458 new String [] { Files.FileColumns._ID, column }, 459 ID_WHERE, new String[] { Integer.toString(id) }, null); 460 if (c != null && c.moveToNext()) { 461 outValue[0] = c.getLong(1); 462 return true; 463 } 464 return false; 465 } catch (Exception e) { 466 return false; 467 } finally { 468 if (c != null) { 469 c.close(); 470 } 471 } 472 } 473 474 private String nameFromPath(String path) { 475 // extract name from full path 476 int start = 0; 477 int lastSlash = path.lastIndexOf('/'); 478 if (lastSlash >= 0) { 479 start = lastSlash + 1; 480 } 481 int end = path.length(); 482 if (end - start > 255) { 483 end = start + 255; 484 } 485 return path.substring(start, end); 486 } 487 488 private int renameFile(int handle, String newName) { 489 Cursor c = null; 490 491 // first compute current path 492 String path = null; 493 String[] whereArgs = new String[] { Integer.toString(handle) }; 494 try { 495 c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null); 496 if (c != null && c.moveToNext()) { 497 path = externalToMediaPath(c.getString(1)); 498 } 499 } catch (RemoteException e) { 500 Log.e(TAG, "RemoteException in getObjectFilePath", e); 501 return MtpConstants.RESPONSE_GENERAL_ERROR; 502 } finally { 503 if (c != null) { 504 c.close(); 505 } 506 } 507 if (path == null) { 508 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 509 } 510 511 // now rename the file. make sure this succeeds before updating database 512 File oldFile = new File(path); 513 int lastSlash = path.lastIndexOf('/'); 514 if (lastSlash <= 1) { 515 return MtpConstants.RESPONSE_GENERAL_ERROR; 516 } 517 String newPath = path.substring(0, lastSlash + 1) + newName; 518 File newFile = new File(newPath); 519 boolean success = oldFile.renameTo(newFile); 520 Log.d(TAG, "renaming "+ path + " to " + newPath + (success ? " succeeded" : " failed")); 521 if (!success) { 522 return MtpConstants.RESPONSE_GENERAL_ERROR; 523 } 524 525 // finally update database 526 ContentValues values = new ContentValues(); 527 values.put(Files.FileColumns.DATA, newPath); 528 int updated = 0; 529 try { 530 // note - we are relying on a special case in MediaProvider.update() to update 531 // the paths for all children in the case where this is a directory. 532 updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs); 533 } catch (RemoteException e) { 534 Log.e(TAG, "RemoteException in mMediaProvider.update", e); 535 } 536 if (updated == 0) { 537 Log.e(TAG, "Unable to update path for " + path + " to " + newPath); 538 // this shouldn't happen, but if it does we need to rename the file to its original name 539 newFile.renameTo(oldFile); 540 return MtpConstants.RESPONSE_GENERAL_ERROR; 541 } 542 543 return MtpConstants.RESPONSE_OK; 544 } 545 546 private int getObjectProperty(int handle, int property, 547 long[] outIntValue, char[] outStringValue) { 548 Log.d(TAG, "getObjectProperty: " + property); 549 String column = null; 550 boolean isString = false; 551 552 switch (property) { 553 case MtpConstants.PROPERTY_STORAGE_ID: 554 outIntValue[0] = mStorageID; 555 return MtpConstants.RESPONSE_OK; 556 case MtpConstants.PROPERTY_OBJECT_FORMAT: 557 column = Files.FileColumns.FORMAT; 558 break; 559 case MtpConstants.PROPERTY_PROTECTION_STATUS: 560 // protection status is always 0 561 outIntValue[0] = 0; 562 return MtpConstants.RESPONSE_OK; 563 case MtpConstants.PROPERTY_OBJECT_SIZE: 564 column = Files.FileColumns.SIZE; 565 break; 566 case MtpConstants.PROPERTY_OBJECT_FILE_NAME: 567 // special case - need to extract file name from full path 568 String value = queryString(handle, Files.FileColumns.DATA); 569 if (value != null) { 570 value = nameFromPath(value); 571 value.getChars(0, value.length(), outStringValue, 0); 572 outStringValue[value.length()] = 0; 573 return MtpConstants.RESPONSE_OK; 574 } else { 575 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 576 } 577 case MtpConstants.PROPERTY_NAME: 578 // first try title 579 String name = queryString(handle, MediaColumns.TITLE); 580 // then try name 581 if (name == null) { 582 name = queryString(handle, Audio.PlaylistsColumns.NAME); 583 } 584 // if title and name fail, extract name from full path 585 if (name == null) { 586 name = queryString(handle, Files.FileColumns.DATA); 587 if (name != null) { 588 name = nameFromPath(name); 589 } 590 } 591 if (name != null) { 592 name.getChars(0, name.length(), outStringValue, 0); 593 outStringValue[name.length()] = 0; 594 return MtpConstants.RESPONSE_OK; 595 } else { 596 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 597 } 598 case MtpConstants.PROPERTY_DATE_MODIFIED: 599 column = Files.FileColumns.DATE_MODIFIED; 600 break; 601 case MtpConstants.PROPERTY_DATE_ADDED: 602 column = Files.FileColumns.DATE_ADDED; 603 break; 604 case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE: 605 column = Audio.AudioColumns.YEAR; 606 break; 607 case MtpConstants.PROPERTY_PARENT_OBJECT: 608 column = Files.FileColumns.PARENT; 609 break; 610 case MtpConstants.PROPERTY_PERSISTENT_UID: 611 // PUID is concatenation of storageID and object handle 612 long puid = mStorageID; 613 puid <<= 32; 614 puid += handle; 615 outIntValue[0] = puid; 616 return MtpConstants.RESPONSE_OK; 617 case MtpConstants.PROPERTY_DURATION: 618 column = Audio.AudioColumns.DURATION; 619 break; 620 case MtpConstants.PROPERTY_TRACK: 621 if (queryInt(handle, Audio.AudioColumns.TRACK, outIntValue)) { 622 // track is stored in lower 3 decimal digits 623 outIntValue[0] %= 1000; 624 return MtpConstants.RESPONSE_OK; 625 } else { 626 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 627 } 628 case MtpConstants.PROPERTY_DISPLAY_NAME: 629 column = MediaColumns.DISPLAY_NAME; 630 isString = true; 631 break; 632 case MtpConstants.PROPERTY_ARTIST: 633 column = Audio.AudioColumns.ARTIST; 634 isString = true; 635 break; 636 case MtpConstants.PROPERTY_ALBUM_NAME: 637 column = Audio.AudioColumns.ALBUM; 638 isString = true; 639 break; 640 case MtpConstants.PROPERTY_ALBUM_ARTIST: 641 column = Audio.AudioColumns.ALBUM_ARTIST; 642 isString = true; 643 break; 644 case MtpConstants.PROPERTY_GENRE: 645 String genre = queryGenre(handle); 646 if (genre != null) { 647 genre.getChars(0, genre.length(), outStringValue, 0); 648 outStringValue[genre.length()] = 0; 649 return MtpConstants.RESPONSE_OK; 650 } else { 651 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 652 } 653 case MtpConstants.PROPERTY_COMPOSER: 654 column = Audio.AudioColumns.COMPOSER; 655 isString = true; 656 break; 657 case MtpConstants.PROPERTY_DESCRIPTION: 658 column = Images.ImageColumns.DESCRIPTION; 659 isString = true; 660 break; 661 default: 662 return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED; 663 } 664 665 if (isString) { 666 String value = queryString(handle, column); 667 if (value != null) { 668 value.getChars(0, value.length(), outStringValue, 0); 669 outStringValue[value.length()] = 0; 670 return MtpConstants.RESPONSE_OK; 671 } 672 } else { 673 if (queryInt(handle, column, outIntValue)) { 674 return MtpConstants.RESPONSE_OK; 675 } 676 } 677 // query failed if we get here 678 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 679 } 680 681 private int setObjectProperty(int handle, int property, 682 long intValue, String stringValue) { 683 Log.d(TAG, "setObjectProperty: " + property); 684 685 switch (property) { 686 case MtpConstants.PROPERTY_OBJECT_FILE_NAME: 687 return renameFile(handle, stringValue); 688 689 default: 690 return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED; 691 } 692 } 693 694 private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) { 695 Log.d(TAG, "getDeviceProperty: " + property); 696 697 switch (property) { 698 case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: 699 case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: 700 // writable string properties kept in our device property database 701 Cursor c = null; 702 try { 703 c = mDevicePropDb.query("properties", DEVICE_PROPERTY_PROJECTION, 704 DEVICE_PROPERTY_WHERE, new String[] { Integer.toString(property) }, 705 null, null, null); 706 707 if (c != null && c.moveToNext()) { 708 String value = c.getString(1); 709 int length = value.length(); 710 if (length > 255) { 711 length = 255; 712 } 713 value.getChars(0, length, outStringValue, 0); 714 outStringValue[length] = 0; 715 } else { 716 outStringValue[0] = 0; 717 } 718 return MtpConstants.RESPONSE_OK; 719 } finally { 720 if (c != null) { 721 c.close(); 722 } 723 } 724 } 725 726 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; 727 } 728 729 private int setDeviceProperty(int property, long intValue, String stringValue) { 730 Log.d(TAG, "setDeviceProperty: " + property + " : " + stringValue); 731 732 switch (property) { 733 case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER: 734 case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME: 735 // writable string properties kept in our device property database 736 try { 737 ContentValues values = new ContentValues(); 738 values.put("code", property); 739 values.put("value", stringValue); 740 mDevicePropDb.insert("properties", "code", values); 741 return MtpConstants.RESPONSE_OK; 742 } catch (Exception e) { 743 return MtpConstants.RESPONSE_GENERAL_ERROR; 744 } 745 } 746 747 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED; 748 } 749 750 private boolean getObjectInfo(int handle, int[] outStorageFormatParent, 751 char[] outName, long[] outSizeModified) { 752 Log.d(TAG, "getObjectInfo: " + handle); 753 Cursor c = null; 754 try { 755 c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION, 756 ID_WHERE, new String[] { Integer.toString(handle) }, null); 757 if (c != null && c.moveToNext()) { 758 outStorageFormatParent[0] = mStorageID; 759 outStorageFormatParent[1] = c.getInt(2); 760 outStorageFormatParent[2] = c.getInt(3); 761 762 // extract name from path 763 String path = c.getString(1); 764 int lastSlash = path.lastIndexOf('/'); 765 int start = (lastSlash >= 0 ? lastSlash + 1 : 0); 766 int end = path.length(); 767 if (end - start > 255) { 768 end = start + 255; 769 } 770 path.getChars(start, end, outName, 0); 771 outName[end - start] = 0; 772 773 outSizeModified[0] = c.getLong(4); 774 outSizeModified[1] = c.getLong(5); 775 return true; 776 } 777 } catch (RemoteException e) { 778 Log.e(TAG, "RemoteException in getObjectInfo", e); 779 } finally { 780 if (c != null) { 781 c.close(); 782 } 783 } 784 return false; 785 } 786 787 private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLength) { 788 Log.d(TAG, "getObjectFilePath: " + handle); 789 if (handle == 0) { 790 // special case root directory 791 mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0); 792 outFilePath[mMediaStoragePath.length()] = 0; 793 outFileLength[0] = 0; 794 return MtpConstants.RESPONSE_OK; 795 } 796 Cursor c = null; 797 try { 798 c = mMediaProvider.query(mObjectsUri, PATH_SIZE_PROJECTION, 799 ID_WHERE, new String[] { Integer.toString(handle) }, null); 800 if (c != null && c.moveToNext()) { 801 String path = externalToMediaPath(c.getString(1)); 802 path.getChars(0, path.length(), outFilePath, 0); 803 outFilePath[path.length()] = 0; 804 outFileLength[0] = c.getLong(2); 805 return MtpConstants.RESPONSE_OK; 806 } else { 807 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 808 } 809 } catch (RemoteException e) { 810 Log.e(TAG, "RemoteException in getObjectFilePath", e); 811 return MtpConstants.RESPONSE_GENERAL_ERROR; 812 } finally { 813 if (c != null) { 814 c.close(); 815 } 816 } 817 } 818 819 private int deleteRecursive(int handle) throws RemoteException { 820 int[] children = getObjectList(0 /* storageID */, 0 /* format */, handle); 821 Uri uri = Files.getMtpObjectsUri(mVolumeName, handle); 822 // delete parent first, to avoid potential infinite recursion 823 int count = mMediaProvider.delete(uri, null, null); 824 if (count == 1) { 825 if (children != null) { 826 for (int i = 0; i < children.length; i++) { 827 count += deleteRecursive(children[i]); 828 } 829 } 830 } 831 return count; 832 } 833 834 private int deleteFile(int handle) { 835 Log.d(TAG, "deleteFile: " + handle); 836 mDatabaseModified = true; 837 try { 838 if (deleteRecursive(handle) > 0) { 839 return MtpConstants.RESPONSE_OK; 840 } else { 841 return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE; 842 } 843 } catch (RemoteException e) { 844 Log.e(TAG, "RemoteException in deleteFile", e); 845 return MtpConstants.RESPONSE_GENERAL_ERROR; 846 } 847 } 848 849 private int[] getObjectReferences(int handle) { 850 Log.d(TAG, "getObjectReferences for: " + handle); 851 Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); 852 Cursor c = null; 853 try { 854 c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null); 855 if (c == null) { 856 return null; 857 } 858 int count = c.getCount(); 859 if (count > 0) { 860 int[] result = new int[count]; 861 for (int i = 0; i < count; i++) { 862 c.moveToNext(); 863 result[i] = c.getInt(0); 864 } 865 return result; 866 } 867 } catch (RemoteException e) { 868 Log.e(TAG, "RemoteException in getObjectList", e); 869 } finally { 870 if (c != null) { 871 c.close(); 872 } 873 } 874 return null; 875 } 876 877 private int setObjectReferences(int handle, int[] references) { 878 mDatabaseModified = true; 879 Uri uri = Files.getMtpReferencesUri(mVolumeName, handle); 880 int count = references.length; 881 ContentValues[] valuesList = new ContentValues[count]; 882 for (int i = 0; i < count; i++) { 883 ContentValues values = new ContentValues(); 884 values.put(Files.FileColumns._ID, references[i]); 885 valuesList[i] = values; 886 } 887 try { 888 if (count == mMediaProvider.bulkInsert(uri, valuesList)) { 889 return MtpConstants.RESPONSE_OK; 890 } 891 } catch (RemoteException e) { 892 Log.e(TAG, "RemoteException in setObjectReferences", e); 893 } 894 return MtpConstants.RESPONSE_GENERAL_ERROR; 895 } 896 897 private void sessionStarted() { 898 Log.d(TAG, "sessionStarted"); 899 mDatabaseModified = false; 900 } 901 902 private void sessionEnded() { 903 Log.d(TAG, "sessionEnded"); 904 if (mDatabaseModified) { 905 Log.d(TAG, "sending ACTION_MTP_SESSION_END"); 906 mContext.sendBroadcast(new Intent(Mtp.ACTION_MTP_SESSION_END)); 907 mDatabaseModified = false; 908 } 909 } 910 911 // used by the JNI code 912 private int mNativeContext; 913 914 private native final void native_setup(); 915 private native final void native_finalize(); 916} 917