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