MediaScanner.java revision e1bf8efaff42dc33b7a4663f3c9d50d12de81bcb
1/* 2 * Copyright (C) 2007 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 17package android.media; 18 19import org.xml.sax.Attributes; 20import org.xml.sax.ContentHandler; 21import org.xml.sax.SAXException; 22 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.IContentProvider; 27import android.database.Cursor; 28import android.database.SQLException; 29import android.drm.DrmManagerClient; 30import android.graphics.BitmapFactory; 31import android.mtp.MtpConstants; 32import android.net.Uri; 33import android.os.Environment; 34import android.os.Process; 35import android.os.RemoteException; 36import android.os.SystemProperties; 37import android.provider.MediaStore; 38import android.provider.Settings; 39import android.provider.MediaStore.Audio; 40import android.provider.MediaStore.Files; 41import android.provider.MediaStore.Images; 42import android.provider.MediaStore.Video; 43import android.provider.MediaStore.Audio.Playlists; 44import android.sax.Element; 45import android.sax.ElementListener; 46import android.sax.RootElement; 47import android.text.TextUtils; 48import android.util.Log; 49import android.util.Xml; 50 51import java.io.BufferedReader; 52import java.io.File; 53import java.io.FileDescriptor; 54import java.io.FileInputStream; 55import java.io.IOException; 56import java.io.InputStreamReader; 57import java.util.ArrayList; 58import java.util.HashMap; 59import java.util.HashSet; 60import java.util.Iterator; 61 62/** 63 * Internal service helper that no-one should use directly. 64 * 65 * The way the scan currently works is: 66 * - The Java MediaScannerService creates a MediaScanner (this class), and calls 67 * MediaScanner.scanDirectories on it. 68 * - scanDirectories() calls the native processDirectory() for each of the specified directories. 69 * - the processDirectory() JNI method wraps the provided mediascanner client in a native 70 * 'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner 71 * object (which got created when the Java MediaScanner was created). 72 * - native MediaScanner.processDirectory() (currently part of opencore) calls 73 * doProcessDirectory(), which recurses over the folder, and calls 74 * native MyMediaScannerClient.scanFile() for every file whose extension matches. 75 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile, 76 * which calls doScanFile, which after some setup calls back down to native code, calling 77 * MediaScanner.processFile(). 78 * - MediaScanner.processFile() calls one of several methods, depending on the type of the 79 * file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA. 80 * - each of these methods gets metadata key/value pairs from the file, and repeatedly 81 * calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java 82 * counterparts in this file. 83 * - Java handleStringTag() gathers the key/value pairs that it's interested in. 84 * - once processFile returns and we're back in Java code in doScanFile(), it calls 85 * Java MyMediaScannerClient.endFile(), which takes all the data that's been 86 * gathered and inserts an entry in to the database. 87 * 88 * In summary: 89 * Java MediaScannerService calls 90 * Java MediaScanner scanDirectories, which calls 91 * Java MediaScanner processDirectory (native method), which calls 92 * native MediaScanner processDirectory, which calls 93 * native MyMediaScannerClient scanFile, which calls 94 * Java MyMediaScannerClient scanFile, which calls 95 * Java MediaScannerClient doScanFile, which calls 96 * Java MediaScanner processFile (native method), which calls 97 * native MediaScanner processFile, which calls 98 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls 99 * native MyMediaScanner handleStringTag, which calls 100 * Java MyMediaScanner handleStringTag. 101 * Once MediaScanner processFile returns, an entry is inserted in to the database. 102 * 103 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner. 104 * 105 * {@hide} 106 */ 107public class MediaScanner 108{ 109 static { 110 System.loadLibrary("media_jni"); 111 native_init(); 112 } 113 114 private final static String TAG = "MediaScanner"; 115 116 private static final String[] FILES_PRESCAN_PROJECTION = new String[] { 117 Files.FileColumns._ID, // 0 118 Files.FileColumns.DATA, // 1 119 Files.FileColumns.FORMAT, // 2 120 Files.FileColumns.DATE_MODIFIED, // 3 121 }; 122 123 private static final String[] ID_PROJECTION = new String[] { 124 Files.FileColumns._ID, 125 }; 126 127 private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0; 128 private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1; 129 private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2; 130 private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3; 131 132 private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] { 133 Audio.Playlists.Members.PLAYLIST_ID, // 0 134 }; 135 136 private static final int ID_PLAYLISTS_COLUMN_INDEX = 0; 137 private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1; 138 private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2; 139 140 private static final String RINGTONES_DIR = "/ringtones/"; 141 private static final String NOTIFICATIONS_DIR = "/notifications/"; 142 private static final String ALARMS_DIR = "/alarms/"; 143 private static final String MUSIC_DIR = "/music/"; 144 private static final String PODCAST_DIR = "/podcasts/"; 145 146 private static final String[] ID3_GENRES = { 147 // ID3v1 Genres 148 "Blues", 149 "Classic Rock", 150 "Country", 151 "Dance", 152 "Disco", 153 "Funk", 154 "Grunge", 155 "Hip-Hop", 156 "Jazz", 157 "Metal", 158 "New Age", 159 "Oldies", 160 "Other", 161 "Pop", 162 "R&B", 163 "Rap", 164 "Reggae", 165 "Rock", 166 "Techno", 167 "Industrial", 168 "Alternative", 169 "Ska", 170 "Death Metal", 171 "Pranks", 172 "Soundtrack", 173 "Euro-Techno", 174 "Ambient", 175 "Trip-Hop", 176 "Vocal", 177 "Jazz+Funk", 178 "Fusion", 179 "Trance", 180 "Classical", 181 "Instrumental", 182 "Acid", 183 "House", 184 "Game", 185 "Sound Clip", 186 "Gospel", 187 "Noise", 188 "AlternRock", 189 "Bass", 190 "Soul", 191 "Punk", 192 "Space", 193 "Meditative", 194 "Instrumental Pop", 195 "Instrumental Rock", 196 "Ethnic", 197 "Gothic", 198 "Darkwave", 199 "Techno-Industrial", 200 "Electronic", 201 "Pop-Folk", 202 "Eurodance", 203 "Dream", 204 "Southern Rock", 205 "Comedy", 206 "Cult", 207 "Gangsta", 208 "Top 40", 209 "Christian Rap", 210 "Pop/Funk", 211 "Jungle", 212 "Native American", 213 "Cabaret", 214 "New Wave", 215 "Psychadelic", 216 "Rave", 217 "Showtunes", 218 "Trailer", 219 "Lo-Fi", 220 "Tribal", 221 "Acid Punk", 222 "Acid Jazz", 223 "Polka", 224 "Retro", 225 "Musical", 226 "Rock & Roll", 227 "Hard Rock", 228 // The following genres are Winamp extensions 229 "Folk", 230 "Folk-Rock", 231 "National Folk", 232 "Swing", 233 "Fast Fusion", 234 "Bebob", 235 "Latin", 236 "Revival", 237 "Celtic", 238 "Bluegrass", 239 "Avantgarde", 240 "Gothic Rock", 241 "Progressive Rock", 242 "Psychedelic Rock", 243 "Symphonic Rock", 244 "Slow Rock", 245 "Big Band", 246 "Chorus", 247 "Easy Listening", 248 "Acoustic", 249 "Humour", 250 "Speech", 251 "Chanson", 252 "Opera", 253 "Chamber Music", 254 "Sonata", 255 "Symphony", 256 "Booty Bass", 257 "Primus", 258 "Porn Groove", 259 "Satire", 260 "Slow Jam", 261 "Club", 262 "Tango", 263 "Samba", 264 "Folklore", 265 "Ballad", 266 "Power Ballad", 267 "Rhythmic Soul", 268 "Freestyle", 269 "Duet", 270 "Punk Rock", 271 "Drum Solo", 272 "A capella", 273 "Euro-House", 274 "Dance Hall", 275 // The following ones seem to be fairly widely supported as well 276 "Goa", 277 "Drum & Bass", 278 "Club-House", 279 "Hardcore", 280 "Terror", 281 "Indie", 282 "Britpop", 283 "Negerpunk", 284 "Polsk Punk", 285 "Beat", 286 "Christian Gangsta", 287 "Heavy Metal", 288 "Black Metal", 289 "Crossover", 290 "Contemporary Christian", 291 "Christian Rock", 292 "Merengue", 293 "Salsa", 294 "Thrash Metal", 295 "Anime", 296 "JPop", 297 "Synthpop", 298 // 148 and up don't seem to have been defined yet. 299 }; 300 301 private int mNativeContext; 302 private Context mContext; 303 private IContentProvider mMediaProvider; 304 private Uri mAudioUri; 305 private Uri mVideoUri; 306 private Uri mImagesUri; 307 private Uri mThumbsUri; 308 private Uri mPlaylistsUri; 309 private Uri mFilesUri; 310 private boolean mProcessPlaylists, mProcessGenres; 311 private int mMtpObjectHandle; 312 313 private final String mExternalStoragePath; 314 315 // WARNING: Bulk inserts sounded like a great idea and gave us a good performance improvement, 316 // but unfortunately it also introduced a number of bugs. Many of those bugs were fixed, 317 // but (at least) two problems are still outstanding: 318 // 319 // 1) Bulk inserts broke the code that sets the default ringtones on first boot 320 // 2) Bulk inserts broke file based playlists in the case where the playlist is processed 321 // at the same time the files in the playlist are inserted in the database 322 // 323 // These problems might be solvable by moving the logic to the media provider instead, 324 // but for now we are disabling bulk inserts until we have solid fixes for these problems. 325 private static final boolean ENABLE_BULK_INSERTS = false; 326 327 // used when scanning the image database so we know whether we have to prune 328 // old thumbnail files 329 private int mOriginalCount; 330 /** Whether the database had any entries in it before the scan started */ 331 private boolean mWasEmptyPriorToScan = false; 332 /** Whether the scanner has set a default sound for the ringer ringtone. */ 333 private boolean mDefaultRingtoneSet; 334 /** Whether the scanner has set a default sound for the notification ringtone. */ 335 private boolean mDefaultNotificationSet; 336 /** Whether the scanner has set a default sound for the alarm ringtone. */ 337 private boolean mDefaultAlarmSet; 338 /** The filename for the default sound for the ringer ringtone. */ 339 private String mDefaultRingtoneFilename; 340 /** The filename for the default sound for the notification ringtone. */ 341 private String mDefaultNotificationFilename; 342 /** The filename for the default sound for the alarm ringtone. */ 343 private String mDefaultAlarmAlertFilename; 344 /** 345 * The prefix for system properties that define the default sound for 346 * ringtones. Concatenate the name of the setting from Settings 347 * to get the full system property. 348 */ 349 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; 350 351 // set to true if file path comparisons should be case insensitive. 352 // this should be set when scanning files on a case insensitive file system. 353 private boolean mCaseInsensitivePaths; 354 355 private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); 356 357 private static class FileCacheEntry { 358 long mRowId; 359 String mPath; 360 long mLastModified; 361 int mFormat; 362 boolean mSeenInFileSystem; 363 boolean mLastModifiedChanged; 364 365 FileCacheEntry(long rowId, String path, long lastModified, int format) { 366 mRowId = rowId; 367 mPath = path; 368 mLastModified = lastModified; 369 mFormat = format; 370 mSeenInFileSystem = false; 371 mLastModifiedChanged = false; 372 } 373 374 @Override 375 public String toString() { 376 return mPath + " mRowId: " + mRowId; 377 } 378 } 379 380 private class FileInserter { 381 382 private final Uri mUri; 383 private final ContentValues[] mValues; 384 private int mIndex; 385 386 public FileInserter(Uri uri, int count) { 387 mUri = uri; 388 mValues = new ContentValues[count]; 389 } 390 391 public Uri insert(ContentValues values) { 392 if (mIndex == mValues.length) { 393 flush(); 394 } 395 mValues[mIndex++] = values; 396 // URI not needed when doing bulk inserts 397 return null; 398 } 399 400 public void flush() { 401 while (mIndex < mValues.length) { 402 mValues[mIndex++] = null; 403 } 404 try { 405 mMediaProvider.bulkInsert(mUri, mValues); 406 } catch (RemoteException e) { 407 Log.e(TAG, "RemoteException in FileInserter.flush()", e); 408 } 409 mIndex = 0; 410 } 411 } 412 413 private FileInserter mAudioInserter; 414 private FileInserter mVideoInserter; 415 private FileInserter mImageInserter; 416 private FileInserter mFileInserter; 417 418 // hashes file path to FileCacheEntry. 419 // path should be lower case if mCaseInsensitivePaths is true 420 private HashMap<String, FileCacheEntry> mFileCache; 421 422 private ArrayList<FileCacheEntry> mPlayLists; 423 424 private DrmManagerClient mDrmManagerClient = null; 425 426 public MediaScanner(Context c) { 427 native_setup(); 428 mContext = c; 429 mBitmapOptions.inSampleSize = 1; 430 mBitmapOptions.inJustDecodeBounds = true; 431 432 setDefaultRingtoneFileNames(); 433 434 mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath(); 435 } 436 437 private void setDefaultRingtoneFileNames() { 438 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 439 + Settings.System.RINGTONE); 440 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 441 + Settings.System.NOTIFICATION_SOUND); 442 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 443 + Settings.System.ALARM_ALERT); 444 } 445 446 private MyMediaScannerClient mClient = new MyMediaScannerClient(); 447 448 private boolean isDrmEnabled() { 449 String prop = SystemProperties.get("drm.service.enabled"); 450 return prop != null && prop.equals("true"); 451 } 452 453 private class MyMediaScannerClient implements MediaScannerClient { 454 455 private String mArtist; 456 private String mAlbumArtist; // use this if mArtist is missing 457 private String mAlbum; 458 private String mTitle; 459 private String mComposer; 460 private String mGenre; 461 private String mMimeType; 462 private int mFileType; 463 private int mTrack; 464 private int mYear; 465 private int mDuration; 466 private String mPath; 467 private long mLastModified; 468 private long mFileSize; 469 private String mWriter; 470 private int mCompilation; 471 private boolean mIsDrm; 472 private boolean mNoMedia; // flag to suppress file from appearing in media tables 473 private int mWidth; 474 private int mHeight; 475 476 public FileCacheEntry beginFile(String path, String mimeType, long lastModified, 477 long fileSize, boolean isDirectory, boolean noMedia) { 478 mMimeType = mimeType; 479 mFileType = 0; 480 mFileSize = fileSize; 481 482 if (!isDirectory) { 483 if (!noMedia && isNoMediaFile(path)) { 484 noMedia = true; 485 } 486 mNoMedia = noMedia; 487 488 // try mimeType first, if it is specified 489 if (mimeType != null) { 490 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 491 } 492 493 // if mimeType was not specified, compute file type based on file extension. 494 if (mFileType == 0) { 495 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 496 if (mediaFileType != null) { 497 mFileType = mediaFileType.fileType; 498 if (mMimeType == null) { 499 mMimeType = mediaFileType.mimeType; 500 } 501 } 502 } 503 504 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) { 505 mFileType = getFileTypeFromDrm(path); 506 } 507 } 508 509 String key = path; 510 if (mCaseInsensitivePaths) { 511 key = path.toLowerCase(); 512 } 513 FileCacheEntry entry = mFileCache.get(key); 514 // add some slack to avoid a rounding error 515 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0; 516 boolean wasModified = delta > 1 || delta < -1; 517 if (entry == null || wasModified) { 518 if (wasModified) { 519 entry.mLastModified = lastModified; 520 } else { 521 entry = new FileCacheEntry(0, path, lastModified, 522 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0)); 523 mFileCache.put(key, entry); 524 } 525 entry.mLastModifiedChanged = true; 526 } 527 entry.mSeenInFileSystem = true; 528 529 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 530 mPlayLists.add(entry); 531 // we don't process playlists in the main scan, so return null 532 return null; 533 } 534 535 // clear all the metadata 536 mArtist = null; 537 mAlbumArtist = null; 538 mAlbum = null; 539 mTitle = null; 540 mComposer = null; 541 mGenre = null; 542 mTrack = 0; 543 mYear = 0; 544 mDuration = 0; 545 mPath = path; 546 mLastModified = lastModified; 547 mWriter = null; 548 mCompilation = 0; 549 mIsDrm = false; 550 mWidth = 0; 551 mHeight = 0; 552 553 return entry; 554 } 555 556 @Override 557 public void scanFile(String path, long lastModified, long fileSize, 558 boolean isDirectory, boolean noMedia) { 559 // This is the callback funtion from native codes. 560 // Log.v(TAG, "scanFile: "+path); 561 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia); 562 } 563 564 public Uri doScanFile(String path, String mimeType, long lastModified, 565 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 566 Uri result = null; 567// long t1 = System.currentTimeMillis(); 568 try { 569 FileCacheEntry entry = beginFile(path, mimeType, lastModified, 570 fileSize, isDirectory, noMedia); 571 // rescan for metadata if file was modified since last scan 572 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 573 if (noMedia) { 574 result = endFile(entry, false, false, false, false, false); 575 } else { 576 String lowpath = path.toLowerCase(); 577 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 578 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 579 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 580 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 581 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 582 (!ringtones && !notifications && !alarms && !podcasts); 583 584 // we only extract metadata for audio and video files 585 if (MediaFile.isAudioFileType(mFileType) 586 || MediaFile.isVideoFileType(mFileType)) { 587 processFile(path, mimeType, this); 588 } 589 590 if (MediaFile.isImageFileType(mFileType)) { 591 processImageFile(path); 592 } 593 594 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 595 } 596 } 597 } catch (RemoteException e) { 598 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 599 } 600// long t2 = System.currentTimeMillis(); 601// Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 602 return result; 603 } 604 605 private int parseSubstring(String s, int start, int defaultValue) { 606 int length = s.length(); 607 if (start == length) return defaultValue; 608 609 char ch = s.charAt(start++); 610 // return defaultValue if we have no integer at all 611 if (ch < '0' || ch > '9') return defaultValue; 612 613 int result = ch - '0'; 614 while (start < length) { 615 ch = s.charAt(start++); 616 if (ch < '0' || ch > '9') return result; 617 result = result * 10 + (ch - '0'); 618 } 619 620 return result; 621 } 622 623 public void handleStringTag(String name, String value) { 624 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 625 // Don't trim() here, to preserve the special \001 character 626 // used to force sorting. The media provider will trim() before 627 // inserting the title in to the database. 628 mTitle = value; 629 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 630 mArtist = value.trim(); 631 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") 632 || name.equalsIgnoreCase("band") || name.startsWith("band;")) { 633 mAlbumArtist = value.trim(); 634 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 635 mAlbum = value.trim(); 636 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 637 mComposer = value.trim(); 638 } else if (mProcessGenres && 639 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { 640 mGenre = getGenreName(value); 641 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 642 mYear = parseSubstring(value, 0, 0); 643 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 644 // track number might be of the form "2/12" 645 // we just read the number before the slash 646 int num = parseSubstring(value, 0, 0); 647 mTrack = (mTrack / 1000) * 1000 + num; 648 } else if (name.equalsIgnoreCase("discnumber") || 649 name.equals("set") || name.startsWith("set;")) { 650 // set number might be of the form "1/3" 651 // we just read the number before the slash 652 int num = parseSubstring(value, 0, 0); 653 mTrack = (num * 1000) + (mTrack % 1000); 654 } else if (name.equalsIgnoreCase("duration")) { 655 mDuration = parseSubstring(value, 0, 0); 656 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) { 657 mWriter = value.trim(); 658 } else if (name.equalsIgnoreCase("compilation")) { 659 mCompilation = parseSubstring(value, 0, 0); 660 } else if (name.equalsIgnoreCase("isdrm")) { 661 mIsDrm = (parseSubstring(value, 0, 0) == 1); 662 } 663 } 664 665 public String getGenreName(String genreTagValue) { 666 667 if (genreTagValue == null) { 668 return null; 669 } 670 final int length = genreTagValue.length(); 671 672 if (length > 0 && genreTagValue.charAt(0) == '(') { 673 StringBuffer number = new StringBuffer(); 674 int i = 1; 675 for (; i < length - 1; ++i) { 676 char c = genreTagValue.charAt(i); 677 if (Character.isDigit(c)) { 678 number.append(c); 679 } else { 680 break; 681 } 682 } 683 if (genreTagValue.charAt(i) == ')') { 684 try { 685 short genreIndex = Short.parseShort(number.toString()); 686 if (genreIndex >= 0) { 687 if (genreIndex < ID3_GENRES.length) { 688 return ID3_GENRES[genreIndex]; 689 } else if (genreIndex == 0xFF) { 690 return null; 691 } else if (genreIndex < 0xFF && (i + 1) < length) { 692 // genre is valid but unknown, 693 // if there is a string after the value we take it 694 return genreTagValue.substring(i + 1); 695 } else { 696 // else return the number, without parentheses 697 return number.toString(); 698 } 699 } 700 } catch (NumberFormatException e) { 701 } 702 } 703 } 704 705 return genreTagValue; 706 } 707 708 private void processImageFile(String path) { 709 try { 710 mBitmapOptions.outWidth = 0; 711 mBitmapOptions.outHeight = 0; 712 BitmapFactory.decodeFile(path, mBitmapOptions); 713 mWidth = mBitmapOptions.outWidth; 714 mHeight = mBitmapOptions.outHeight; 715 } catch (Throwable th) { 716 // ignore; 717 } 718 } 719 720 public void setMimeType(String mimeType) { 721 if ("audio/mp4".equals(mMimeType) && 722 mimeType.startsWith("video")) { 723 // for feature parity with Donut, we force m4a files to keep the 724 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 725 // with a video track 726 return; 727 } 728 mMimeType = mimeType; 729 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 730 } 731 732 /** 733 * Formats the data into a values array suitable for use with the Media 734 * Content Provider. 735 * 736 * @return a map of values 737 */ 738 private ContentValues toValues() { 739 ContentValues map = new ContentValues(); 740 741 map.put(MediaStore.MediaColumns.DATA, mPath); 742 map.put(MediaStore.MediaColumns.TITLE, mTitle); 743 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 744 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 745 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 746 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm); 747 748 if (mWidth > 0 && mHeight > 0) { 749 map.put(MediaStore.MediaColumns.WIDTH, mWidth); 750 map.put(MediaStore.MediaColumns.HEIGHT, mHeight); 751 } 752 753 if (!mNoMedia) { 754 if (MediaFile.isVideoFileType(mFileType)) { 755 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 756 ? mArtist : MediaStore.UNKNOWN_STRING)); 757 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 758 ? mAlbum : MediaStore.UNKNOWN_STRING)); 759 map.put(Video.Media.DURATION, mDuration); 760 // FIXME - add RESOLUTION 761 } else if (MediaFile.isImageFileType(mFileType)) { 762 // FIXME - add DESCRIPTION 763 } else if (MediaFile.isAudioFileType(mFileType)) { 764 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 765 mArtist : MediaStore.UNKNOWN_STRING); 766 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 767 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 768 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 769 mAlbum : MediaStore.UNKNOWN_STRING); 770 map.put(Audio.Media.COMPOSER, mComposer); 771 map.put(Audio.Media.GENRE, mGenre); 772 if (mYear != 0) { 773 map.put(Audio.Media.YEAR, mYear); 774 } 775 map.put(Audio.Media.TRACK, mTrack); 776 map.put(Audio.Media.DURATION, mDuration); 777 map.put(Audio.Media.COMPILATION, mCompilation); 778 } 779 } 780 return map; 781 } 782 783 private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, 784 boolean alarms, boolean music, boolean podcasts) 785 throws RemoteException { 786 // update database 787 788 // use album artist if artist is missing 789 if (mArtist == null || mArtist.length() == 0) { 790 mArtist = mAlbumArtist; 791 } 792 793 ContentValues values = toValues(); 794 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 795 if (title == null || TextUtils.isEmpty(title.trim())) { 796 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 797 values.put(MediaStore.MediaColumns.TITLE, title); 798 } 799 String album = values.getAsString(Audio.Media.ALBUM); 800 if (MediaStore.UNKNOWN_STRING.equals(album)) { 801 album = values.getAsString(MediaStore.MediaColumns.DATA); 802 // extract last path segment before file name 803 int lastSlash = album.lastIndexOf('/'); 804 if (lastSlash >= 0) { 805 int previousSlash = 0; 806 while (true) { 807 int idx = album.indexOf('/', previousSlash + 1); 808 if (idx < 0 || idx >= lastSlash) { 809 break; 810 } 811 previousSlash = idx; 812 } 813 if (previousSlash != 0) { 814 album = album.substring(previousSlash + 1, lastSlash); 815 values.put(Audio.Media.ALBUM, album); 816 } 817 } 818 } 819 long rowId = entry.mRowId; 820 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) { 821 // Only set these for new entries. For existing entries, they 822 // may have been modified later, and we want to keep the current 823 // values so that custom ringtones still show up in the ringtone 824 // picker. 825 values.put(Audio.Media.IS_RINGTONE, ringtones); 826 values.put(Audio.Media.IS_NOTIFICATION, notifications); 827 values.put(Audio.Media.IS_ALARM, alarms); 828 values.put(Audio.Media.IS_MUSIC, music); 829 values.put(Audio.Media.IS_PODCAST, podcasts); 830 } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) { 831 ExifInterface exif = null; 832 try { 833 exif = new ExifInterface(entry.mPath); 834 } catch (IOException ex) { 835 // exif is null 836 } 837 if (exif != null) { 838 float[] latlng = new float[2]; 839 if (exif.getLatLong(latlng)) { 840 values.put(Images.Media.LATITUDE, latlng[0]); 841 values.put(Images.Media.LONGITUDE, latlng[1]); 842 } 843 844 long time = exif.getGpsDateTime(); 845 if (time != -1) { 846 values.put(Images.Media.DATE_TAKEN, time); 847 } else { 848 // If no time zone information is available, we should consider using 849 // EXIF local time as taken time if the difference between file time 850 // and EXIF local time is not less than 1 Day, otherwise MediaProvider 851 // will use file time as taken time. 852 time = exif.getDateTime(); 853 if (Math.abs(mLastModified * 1000 - time) >= 86400000) { 854 values.put(Images.Media.DATE_TAKEN, time); 855 } 856 } 857 858 int orientation = exif.getAttributeInt( 859 ExifInterface.TAG_ORIENTATION, -1); 860 if (orientation != -1) { 861 // We only recognize a subset of orientation tag values. 862 int degree; 863 switch(orientation) { 864 case ExifInterface.ORIENTATION_ROTATE_90: 865 degree = 90; 866 break; 867 case ExifInterface.ORIENTATION_ROTATE_180: 868 degree = 180; 869 break; 870 case ExifInterface.ORIENTATION_ROTATE_270: 871 degree = 270; 872 break; 873 default: 874 degree = 0; 875 break; 876 } 877 values.put(Images.Media.ORIENTATION, degree); 878 } 879 } 880 } 881 882 Uri tableUri = mFilesUri; 883 FileInserter inserter = mFileInserter; 884 if (!mNoMedia) { 885 if (MediaFile.isVideoFileType(mFileType)) { 886 tableUri = mVideoUri; 887 inserter = mVideoInserter; 888 } else if (MediaFile.isImageFileType(mFileType)) { 889 tableUri = mImagesUri; 890 inserter = mImageInserter; 891 } else if (MediaFile.isAudioFileType(mFileType)) { 892 tableUri = mAudioUri; 893 inserter = mAudioInserter; 894 } 895 } 896 Uri result = null; 897 if (rowId == 0) { 898 if (mMtpObjectHandle != 0) { 899 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle); 900 } 901 if (tableUri == mFilesUri) { 902 int format = entry.mFormat; 903 if (format == 0) { 904 format = MediaFile.getFormatCode(entry.mPath, mMimeType); 905 } 906 values.put(Files.FileColumns.FORMAT, format); 907 } 908 // new file, insert it 909 // We insert directories immediately to ensure they are in the database 910 // before the files they contain. 911 // Otherwise we can get duplicate directory entries in the database 912 // if one of the media FileInserters is flushed before the files table FileInserter 913 if (inserter == null || entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 914 result = mMediaProvider.insert(tableUri, values); 915 } else { 916 result = inserter.insert(values); 917 } 918 919 if (result != null) { 920 rowId = ContentUris.parseId(result); 921 entry.mRowId = rowId; 922 } 923 } else { 924 // updated file 925 result = ContentUris.withAppendedId(tableUri, rowId); 926 // path should never change, and we want to avoid replacing mixed cased paths 927 // with squashed lower case paths 928 values.remove(MediaStore.MediaColumns.DATA); 929 mMediaProvider.update(result, values, null, null); 930 } 931 932 if (notifications && mWasEmptyPriorToScan && !mDefaultNotificationSet) { 933 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 934 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 935 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 936 mDefaultNotificationSet = true; 937 } 938 } else if (ringtones && mWasEmptyPriorToScan && !mDefaultRingtoneSet) { 939 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 940 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 941 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 942 mDefaultRingtoneSet = true; 943 } 944 } else if (alarms && mWasEmptyPriorToScan && !mDefaultAlarmSet) { 945 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 946 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 947 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 948 mDefaultAlarmSet = true; 949 } 950 } 951 952 return result; 953 } 954 955 private boolean doesPathHaveFilename(String path, String filename) { 956 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 957 int filenameLength = filename.length(); 958 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 959 pathFilenameStart + filenameLength == path.length(); 960 } 961 962 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 963 964 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 965 settingName); 966 967 if (TextUtils.isEmpty(existingSettingValue)) { 968 // Set the setting to the given URI 969 Settings.System.putString(mContext.getContentResolver(), settingName, 970 ContentUris.withAppendedId(uri, rowId).toString()); 971 } 972 } 973 974 private int getFileTypeFromDrm(String path) { 975 if (!isDrmEnabled()) { 976 return 0; 977 } 978 979 int resultFileType = 0; 980 981 if (mDrmManagerClient == null) { 982 mDrmManagerClient = new DrmManagerClient(mContext); 983 } 984 985 if (mDrmManagerClient.canHandle(path, null)) { 986 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path); 987 if (drmMimetype != null) { 988 mMimeType = drmMimetype; 989 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype); 990 } 991 } 992 return resultFileType; 993 } 994 995 }; // end of anonymous MediaScannerClient instance 996 997 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 998 Cursor c = null; 999 String where = null; 1000 String[] selectionArgs = null; 1001 1002 if (mFileCache == null) { 1003 mFileCache = new HashMap<String, FileCacheEntry>(); 1004 } else { 1005 mFileCache.clear(); 1006 } 1007 if (mPlayLists == null) { 1008 mPlayLists = new ArrayList<FileCacheEntry>(); 1009 } else { 1010 mPlayLists.clear(); 1011 } 1012 1013 if (filePath != null) { 1014 // query for only one file 1015 where = Files.FileColumns.DATA + "=?"; 1016 selectionArgs = new String[] { filePath }; 1017 } 1018 1019 // Build the list of files from the content provider 1020 try { 1021 if (prescanFiles) { 1022 // First read existing files from the files table 1023 1024 c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1025 where, selectionArgs, null); 1026 1027 if (c != null) { 1028 mWasEmptyPriorToScan = c.getCount() == 0; 1029 while (c.moveToNext()) { 1030 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1031 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1032 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1033 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1034 1035 // Only consider entries with absolute path names. 1036 // This allows storing URIs in the database without the 1037 // media scanner removing them. 1038 if (path != null && path.startsWith("/")) { 1039 String key = path; 1040 if (mCaseInsensitivePaths) { 1041 key = path.toLowerCase(); 1042 } 1043 1044 FileCacheEntry entry = new FileCacheEntry(rowId, path, 1045 lastModified, format); 1046 mFileCache.put(key, entry); 1047 } 1048 } 1049 c.close(); 1050 c = null; 1051 } 1052 } 1053 } 1054 finally { 1055 if (c != null) { 1056 c.close(); 1057 } 1058 } 1059 1060 // compute original size of images 1061 mOriginalCount = 0; 1062 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null); 1063 if (c != null) { 1064 mOriginalCount = c.getCount(); 1065 c.close(); 1066 } 1067 } 1068 1069 private boolean inScanDirectory(String path, String[] directories) { 1070 for (int i = 0; i < directories.length; i++) { 1071 String directory = directories[i]; 1072 if (path.startsWith(directory)) { 1073 return true; 1074 } 1075 } 1076 return false; 1077 } 1078 1079 private void pruneDeadThumbnailFiles() { 1080 HashSet<String> existingFiles = new HashSet<String>(); 1081 String directory = "/sdcard/DCIM/.thumbnails"; 1082 String [] files = (new File(directory)).list(); 1083 if (files == null) 1084 files = new String[0]; 1085 1086 for (int i = 0; i < files.length; i++) { 1087 String fullPathString = directory + "/" + files[i]; 1088 existingFiles.add(fullPathString); 1089 } 1090 1091 try { 1092 Cursor c = mMediaProvider.query( 1093 mThumbsUri, 1094 new String [] { "_data" }, 1095 null, 1096 null, 1097 null); 1098 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1099 if (c != null && c.moveToFirst()) { 1100 do { 1101 String fullPathString = c.getString(0); 1102 existingFiles.remove(fullPathString); 1103 } while (c.moveToNext()); 1104 } 1105 1106 for (String fileToDelete : existingFiles) { 1107 if (false) 1108 Log.v(TAG, "fileToDelete is " + fileToDelete); 1109 try { 1110 (new File(fileToDelete)).delete(); 1111 } catch (SecurityException ex) { 1112 } 1113 } 1114 1115 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1116 if (c != null) { 1117 c.close(); 1118 } 1119 } catch (RemoteException e) { 1120 // We will soon be killed... 1121 } 1122 } 1123 1124 private void postscan(String[] directories) throws RemoteException { 1125 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1126 1127 while (iterator.hasNext()) { 1128 FileCacheEntry entry = iterator.next(); 1129 String path = entry.mPath; 1130 1131 // remove database entries for files that no longer exist. 1132 boolean fileMissing = false; 1133 1134 if (!entry.mSeenInFileSystem && !MtpConstants.isAbstractObject(entry.mFormat)) { 1135 if (inScanDirectory(path, directories)) { 1136 // we didn't see this file in the scan directory. 1137 fileMissing = true; 1138 } else { 1139 // the file actually a directory or other abstract object 1140 // or is outside of our scan directory, 1141 // so we need to check for file existence here. 1142 File testFile = new File(path); 1143 if (!testFile.exists()) { 1144 fileMissing = true; 1145 } 1146 } 1147 } 1148 1149 if (fileMissing) { 1150 // Clear the file path to prevent the _DELETE_FILE database hook 1151 // in the media provider from deleting the file. 1152 // If the file is truly gone the delete is unnecessary, and we want to avoid 1153 // accidentally deleting files that are really there. 1154 ContentValues values = new ContentValues(); 1155 values.put(Files.FileColumns.DATA, ""); 1156 values.put(Files.FileColumns.DATE_MODIFIED, 0); 1157 mMediaProvider.update(ContentUris.withAppendedId(mFilesUri, entry.mRowId), 1158 values, null, null); 1159 1160 // do not delete missing playlists, since they may have been modified by the user. 1161 // the user can delete them in the media player instead. 1162 // instead, clear the path and lastModified fields in the row 1163 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1164 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1165 1166 if (!MediaFile.isPlayListFileType(fileType)) { 1167 mMediaProvider.delete(ContentUris.withAppendedId(mFilesUri, entry.mRowId), 1168 null, null); 1169 iterator.remove(); 1170 } 1171 } 1172 } 1173 1174 // handle playlists last, after we know what media files are on the storage. 1175 if (mProcessPlaylists) { 1176 processPlayLists(); 1177 } 1178 1179 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1180 pruneDeadThumbnailFiles(); 1181 1182 // allow GC to clean up 1183 mPlayLists = null; 1184 mFileCache = null; 1185 mMediaProvider = null; 1186 } 1187 1188 private void initialize(String volumeName) { 1189 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1190 1191 mAudioUri = Audio.Media.getContentUri(volumeName); 1192 mVideoUri = Video.Media.getContentUri(volumeName); 1193 mImagesUri = Images.Media.getContentUri(volumeName); 1194 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1195 mFilesUri = Files.getContentUri(volumeName); 1196 1197 if (!volumeName.equals("internal")) { 1198 // we only support playlists on external media 1199 mProcessPlaylists = true; 1200 mProcessGenres = true; 1201 mPlaylistsUri = Playlists.getContentUri(volumeName); 1202 1203 mCaseInsensitivePaths = true; 1204 } 1205 } 1206 1207 public void scanDirectories(String[] directories, String volumeName) { 1208 try { 1209 long start = System.currentTimeMillis(); 1210 initialize(volumeName); 1211 prescan(null, true); 1212 long prescan = System.currentTimeMillis(); 1213 1214 if (ENABLE_BULK_INSERTS) { 1215 // create FileInserters for bulk inserts 1216 mAudioInserter = new FileInserter(mAudioUri, 500); 1217 mVideoInserter = new FileInserter(mVideoUri, 500); 1218 mImageInserter = new FileInserter(mImagesUri, 500); 1219 mFileInserter = new FileInserter(mFilesUri, 500); 1220 } 1221 1222 for (int i = 0; i < directories.length; i++) { 1223 processDirectory(directories[i], mClient); 1224 } 1225 1226 if (ENABLE_BULK_INSERTS) { 1227 // flush remaining inserts 1228 mAudioInserter.flush(); 1229 mVideoInserter.flush(); 1230 mImageInserter.flush(); 1231 mFileInserter.flush(); 1232 mAudioInserter = null; 1233 mVideoInserter = null; 1234 mImageInserter = null; 1235 mFileInserter = null; 1236 } 1237 1238 long scan = System.currentTimeMillis(); 1239 postscan(directories); 1240 long end = System.currentTimeMillis(); 1241 1242 if (false) { 1243 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1244 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1245 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1246 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1247 } 1248 } catch (SQLException e) { 1249 // this might happen if the SD card is removed while the media scanner is running 1250 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1251 } catch (UnsupportedOperationException e) { 1252 // this might happen if the SD card is removed while the media scanner is running 1253 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1254 } catch (RemoteException e) { 1255 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1256 } 1257 } 1258 1259 // this function is used to scan a single file 1260 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1261 try { 1262 initialize(volumeName); 1263 prescan(path, true); 1264 1265 File file = new File(path); 1266 1267 // lastModified is in milliseconds on Files. 1268 long lastModifiedSeconds = file.lastModified() / 1000; 1269 1270 // always scan the file, so we can return the content://media Uri for existing files 1271 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1272 false, true, false); 1273 } catch (RemoteException e) { 1274 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1275 return null; 1276 } 1277 } 1278 1279 private static boolean isNoMediaFile(String path) { 1280 File file = new File(path); 1281 if (file.isDirectory()) return false; 1282 1283 // special case certain file names 1284 // I use regionMatches() instead of substring() below 1285 // to avoid memory allocation 1286 int lastSlash = path.lastIndexOf('/'); 1287 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1288 // ignore those ._* files created by MacOS 1289 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1290 return true; 1291 } 1292 1293 // ignore album art files created by Windows Media Player: 1294 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1295 // and AlbumArt_{...}_Small.jpg 1296 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1297 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1298 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1299 return true; 1300 } 1301 int length = path.length() - lastSlash - 1; 1302 if ((length == 17 && path.regionMatches( 1303 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1304 (length == 10 1305 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1306 return true; 1307 } 1308 } 1309 } 1310 return false; 1311 } 1312 1313 public static boolean isNoMediaPath(String path) { 1314 if (path == null) return false; 1315 1316 // return true if file or any parent directory has name starting with a dot 1317 if (path.indexOf("/.") >= 0) return true; 1318 1319 // now check to see if any parent directories have a ".nomedia" file 1320 // start from 1 so we don't bother checking in the root directory 1321 int offset = 1; 1322 while (offset >= 0) { 1323 int slashIndex = path.indexOf('/', offset); 1324 if (slashIndex > offset) { 1325 slashIndex++; // move past slash 1326 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1327 if (file.exists()) { 1328 // we have a .nomedia in one of the parent directories 1329 return true; 1330 } 1331 } 1332 offset = slashIndex; 1333 } 1334 return isNoMediaFile(path); 1335 } 1336 1337 public void scanMtpFile(String path, String volumeName, int objectHandle, int format) { 1338 initialize(volumeName); 1339 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1340 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1341 File file = new File(path); 1342 long lastModifiedSeconds = file.lastModified() / 1000; 1343 1344 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1345 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType)) { 1346 1347 // no need to use the media scanner, but we need to update last modified and file size 1348 ContentValues values = new ContentValues(); 1349 values.put(Files.FileColumns.SIZE, file.length()); 1350 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1351 try { 1352 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1353 mMediaProvider.update(Files.getMtpObjectsUri(volumeName), values, "_id=?", 1354 whereArgs); 1355 } catch (RemoteException e) { 1356 Log.e(TAG, "RemoteException in scanMtpFile", e); 1357 } 1358 return; 1359 } 1360 1361 mMtpObjectHandle = objectHandle; 1362 try { 1363 if (MediaFile.isPlayListFileType(fileType)) { 1364 // build file cache so we can look up tracks in the playlist 1365 prescan(null, true); 1366 1367 String key = path; 1368 if (mCaseInsensitivePaths) { 1369 key = path.toLowerCase(); 1370 } 1371 FileCacheEntry entry = mFileCache.get(key); 1372 if (entry != null) { 1373 processPlayList(entry); 1374 } 1375 } else { 1376 // MTP will create a file entry for us so we don't want to do it in prescan 1377 prescan(path, false); 1378 1379 // always scan the file, so we can return the content://media Uri for existing files 1380 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1381 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1382 } 1383 } catch (RemoteException e) { 1384 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1385 } finally { 1386 mMtpObjectHandle = 0; 1387 } 1388 } 1389 1390 // returns the number of matching file/directory names, starting from the right 1391 private int matchPaths(String path1, String path2) { 1392 int result = 0; 1393 int end1 = path1.length(); 1394 int end2 = path2.length(); 1395 1396 while (end1 > 0 && end2 > 0) { 1397 int slash1 = path1.lastIndexOf('/', end1 - 1); 1398 int slash2 = path2.lastIndexOf('/', end2 - 1); 1399 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1400 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1401 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1402 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1403 if (start1 < 0) start1 = 0; else start1++; 1404 if (start2 < 0) start2 = 0; else start2++; 1405 int length = end1 - start1; 1406 if (end2 - start2 != length) break; 1407 if (path1.regionMatches(true, start1, path2, start2, length)) { 1408 result++; 1409 end1 = start1 - 1; 1410 end2 = start2 - 1; 1411 } else break; 1412 } 1413 1414 return result; 1415 } 1416 1417 private boolean addPlayListEntry(String entry, String playListDirectory, 1418 Uri uri, ContentValues values, int index) { 1419 1420 // watch for trailing whitespace 1421 int entryLength = entry.length(); 1422 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1423 // path should be longer than 3 characters. 1424 // avoid index out of bounds errors below by returning here. 1425 if (entryLength < 3) return false; 1426 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1427 1428 // does entry appear to be an absolute path? 1429 // look for Unix or DOS absolute paths 1430 char ch1 = entry.charAt(0); 1431 boolean fullPath = (ch1 == '/' || 1432 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1433 // if we have a relative path, combine entry with playListDirectory 1434 if (!fullPath) 1435 entry = playListDirectory + entry; 1436 1437 //FIXME - should we look for "../" within the path? 1438 1439 // best matching MediaFile for the play list entry 1440 FileCacheEntry bestMatch = null; 1441 1442 // number of rightmost file/directory names for bestMatch 1443 int bestMatchLength = 0; 1444 1445 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1446 while (iterator.hasNext()) { 1447 FileCacheEntry cacheEntry = iterator.next(); 1448 String path = cacheEntry.mPath; 1449 1450 if (path.equalsIgnoreCase(entry)) { 1451 bestMatch = cacheEntry; 1452 break; // don't bother continuing search 1453 } 1454 1455 int matchLength = matchPaths(path, entry); 1456 if (matchLength > bestMatchLength) { 1457 bestMatch = cacheEntry; 1458 bestMatchLength = matchLength; 1459 } 1460 } 1461 1462 if (bestMatch == null) { 1463 return false; 1464 } 1465 1466 try { 1467 // OK, now we need to add this to the database 1468 values.clear(); 1469 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1470 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1471 mMediaProvider.insert(uri, values); 1472 } catch (RemoteException e) { 1473 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1474 return false; 1475 } 1476 1477 return true; 1478 } 1479 1480 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1481 BufferedReader reader = null; 1482 try { 1483 File f = new File(path); 1484 if (f.exists()) { 1485 reader = new BufferedReader( 1486 new InputStreamReader(new FileInputStream(f)), 8192); 1487 String line = reader.readLine(); 1488 int index = 0; 1489 while (line != null) { 1490 // ignore comment lines, which begin with '#' 1491 if (line.length() > 0 && line.charAt(0) != '#') { 1492 values.clear(); 1493 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1494 index++; 1495 } 1496 line = reader.readLine(); 1497 } 1498 } 1499 } catch (IOException e) { 1500 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1501 } finally { 1502 try { 1503 if (reader != null) 1504 reader.close(); 1505 } catch (IOException e) { 1506 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1507 } 1508 } 1509 } 1510 1511 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1512 BufferedReader reader = null; 1513 try { 1514 File f = new File(path); 1515 if (f.exists()) { 1516 reader = new BufferedReader( 1517 new InputStreamReader(new FileInputStream(f)), 8192); 1518 String line = reader.readLine(); 1519 int index = 0; 1520 while (line != null) { 1521 // ignore comment lines, which begin with '#' 1522 if (line.startsWith("File")) { 1523 int equals = line.indexOf('='); 1524 if (equals > 0) { 1525 values.clear(); 1526 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1527 index++; 1528 } 1529 } 1530 line = reader.readLine(); 1531 } 1532 } 1533 } catch (IOException e) { 1534 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1535 } finally { 1536 try { 1537 if (reader != null) 1538 reader.close(); 1539 } catch (IOException e) { 1540 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1541 } 1542 } 1543 } 1544 1545 class WplHandler implements ElementListener { 1546 1547 final ContentHandler handler; 1548 String playListDirectory; 1549 Uri uri; 1550 ContentValues values = new ContentValues(); 1551 int index = 0; 1552 1553 public WplHandler(String playListDirectory, Uri uri) { 1554 this.playListDirectory = playListDirectory; 1555 this.uri = uri; 1556 1557 RootElement root = new RootElement("smil"); 1558 Element body = root.getChild("body"); 1559 Element seq = body.getChild("seq"); 1560 Element media = seq.getChild("media"); 1561 media.setElementListener(this); 1562 1563 this.handler = root.getContentHandler(); 1564 } 1565 1566 public void start(Attributes attributes) { 1567 String path = attributes.getValue("", "src"); 1568 if (path != null) { 1569 values.clear(); 1570 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1571 index++; 1572 } 1573 } 1574 } 1575 1576 public void end() { 1577 } 1578 1579 ContentHandler getContentHandler() { 1580 return handler; 1581 } 1582 } 1583 1584 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1585 FileInputStream fis = null; 1586 try { 1587 File f = new File(path); 1588 if (f.exists()) { 1589 fis = new FileInputStream(f); 1590 1591 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1592 } 1593 } catch (SAXException e) { 1594 e.printStackTrace(); 1595 } catch (IOException e) { 1596 e.printStackTrace(); 1597 } finally { 1598 try { 1599 if (fis != null) 1600 fis.close(); 1601 } catch (IOException e) { 1602 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1603 } 1604 } 1605 } 1606 1607 private void processPlayList(FileCacheEntry entry) throws RemoteException { 1608 String path = entry.mPath; 1609 ContentValues values = new ContentValues(); 1610 int lastSlash = path.lastIndexOf('/'); 1611 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1612 Uri uri, membersUri; 1613 long rowId = entry.mRowId; 1614 1615 // make sure we have a name 1616 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1617 if (name == null) { 1618 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1619 if (name == null) { 1620 // extract name from file name 1621 int lastDot = path.lastIndexOf('.'); 1622 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1623 : path.substring(lastSlash + 1, lastDot)); 1624 } 1625 } 1626 1627 values.put(MediaStore.Audio.Playlists.NAME, name); 1628 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1629 1630 if (rowId == 0) { 1631 values.put(MediaStore.Audio.Playlists.DATA, path); 1632 uri = mMediaProvider.insert(mPlaylistsUri, values); 1633 rowId = ContentUris.parseId(uri); 1634 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1635 } else { 1636 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1637 mMediaProvider.update(uri, values, null, null); 1638 1639 // delete members of existing playlist 1640 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1641 mMediaProvider.delete(membersUri, null, null); 1642 } 1643 1644 String playListDirectory = path.substring(0, lastSlash + 1); 1645 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1646 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1647 1648 if (fileType == MediaFile.FILE_TYPE_M3U) { 1649 processM3uPlayList(path, playListDirectory, membersUri, values); 1650 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1651 processPlsPlayList(path, playListDirectory, membersUri, values); 1652 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1653 processWplPlayList(path, playListDirectory, membersUri); 1654 } 1655 } 1656 1657 private void processPlayLists() throws RemoteException { 1658 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1659 while (iterator.hasNext()) { 1660 FileCacheEntry entry = iterator.next(); 1661 // only process playlist files if they are new or have been modified since the last scan 1662 if (entry.mLastModifiedChanged) { 1663 processPlayList(entry); 1664 } 1665 } 1666 } 1667 1668 private native void processDirectory(String path, MediaScannerClient client); 1669 private native void processFile(String path, String mimeType, MediaScannerClient client); 1670 public native void setLocale(String locale); 1671 1672 public native byte[] extractAlbumArt(FileDescriptor fd); 1673 1674 private static native final void native_init(); 1675 private native final void native_setup(); 1676 private native final void native_finalize(); 1677 1678 /** 1679 * Releases resouces associated with this MediaScanner object. 1680 * It is considered good practice to call this method when 1681 * one is done using the MediaScanner object. After this method 1682 * is called, the MediaScanner object can no longer be used. 1683 */ 1684 public void release() { 1685 native_finalize(); 1686 } 1687 1688 @Override 1689 protected void finalize() { 1690 mContext.getContentResolver().releaseProvider(mMediaProvider); 1691 native_finalize(); 1692 } 1693} 1694