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