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