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