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