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