MediaScanner.java revision 3410b4656505673c72da558dcfec94d7b135e760
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 scanner has set a default sound for the ringer ringtone. */ 331 private boolean mDefaultRingtoneSet; 332 /** Whether the scanner has set a default sound for the notification ringtone. */ 333 private boolean mDefaultNotificationSet; 334 /** Whether the scanner has set a default sound for the alarm ringtone. */ 335 private boolean mDefaultAlarmSet; 336 /** The filename for the default sound for the ringer ringtone. */ 337 private String mDefaultRingtoneFilename; 338 /** The filename for the default sound for the notification ringtone. */ 339 private String mDefaultNotificationFilename; 340 /** The filename for the default sound for the alarm ringtone. */ 341 private String mDefaultAlarmAlertFilename; 342 /** 343 * The prefix for system properties that define the default sound for 344 * ringtones. Concatenate the name of the setting from Settings 345 * to get the full system property. 346 */ 347 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; 348 349 // set to true if file path comparisons should be case insensitive. 350 // this should be set when scanning files on a case insensitive file system. 351 private boolean mCaseInsensitivePaths; 352 353 private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); 354 355 private static class FileCacheEntry { 356 long mRowId; 357 String mPath; 358 long mLastModified; 359 int mFormat; 360 boolean mSeenInFileSystem; 361 boolean mLastModifiedChanged; 362 363 FileCacheEntry(long rowId, String path, long lastModified, int format) { 364 mRowId = rowId; 365 mPath = path; 366 mLastModified = lastModified; 367 mFormat = format; 368 mSeenInFileSystem = false; 369 mLastModifiedChanged = false; 370 } 371 372 @Override 373 public String toString() { 374 return mPath + " mRowId: " + mRowId; 375 } 376 } 377 378 private class FileInserter { 379 380 private final Uri mUri; 381 private final ContentValues[] mValues; 382 private int mIndex; 383 384 public FileInserter(Uri uri, int count) { 385 mUri = uri; 386 mValues = new ContentValues[count]; 387 } 388 389 public Uri insert(ContentValues values) { 390 if (mIndex == mValues.length) { 391 flush(); 392 } 393 mValues[mIndex++] = values; 394 // URI not needed when doing bulk inserts 395 return null; 396 } 397 398 public void flush() { 399 while (mIndex < mValues.length) { 400 mValues[mIndex++] = null; 401 } 402 try { 403 mMediaProvider.bulkInsert(mUri, mValues); 404 } catch (RemoteException e) { 405 Log.e(TAG, "RemoteException in FileInserter.flush()", e); 406 } 407 mIndex = 0; 408 } 409 } 410 411 private FileInserter mAudioInserter; 412 private FileInserter mVideoInserter; 413 private FileInserter mImageInserter; 414 private FileInserter mFileInserter; 415 416 // hashes file path to FileCacheEntry. 417 // path should be lower case if mCaseInsensitivePaths is true 418 private HashMap<String, FileCacheEntry> mFileCache; 419 420 private ArrayList<FileCacheEntry> mPlayLists; 421 422 private DrmManagerClient mDrmManagerClient = null; 423 424 public MediaScanner(Context c) { 425 native_setup(); 426 mContext = c; 427 mBitmapOptions.inSampleSize = 1; 428 mBitmapOptions.inJustDecodeBounds = true; 429 430 setDefaultRingtoneFileNames(); 431 432 mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath(); 433 } 434 435 private void setDefaultRingtoneFileNames() { 436 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 437 + Settings.System.RINGTONE); 438 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 439 + Settings.System.NOTIFICATION_SOUND); 440 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 441 + Settings.System.ALARM_ALERT); 442 } 443 444 private MyMediaScannerClient mClient = new MyMediaScannerClient(); 445 446 private boolean isDrmEnabled() { 447 String prop = SystemProperties.get("drm.service.enabled"); 448 return prop != null && prop.equals("true"); 449 } 450 451 private class MyMediaScannerClient implements MediaScannerClient { 452 453 private String mArtist; 454 private String mAlbumArtist; // use this if mArtist is missing 455 private String mAlbum; 456 private String mTitle; 457 private String mComposer; 458 private String mGenre; 459 private String mMimeType; 460 private int mFileType; 461 private int mTrack; 462 private int mYear; 463 private int mDuration; 464 private String mPath; 465 private long mLastModified; 466 private long mFileSize; 467 private String mWriter; 468 private int mCompilation; 469 private boolean mIsDrm; 470 private boolean mNoMedia; // flag to suppress file from appearing in media tables 471 472 public FileCacheEntry beginFile(String path, String mimeType, long lastModified, 473 long fileSize, boolean isDirectory, boolean noMedia) { 474 mMimeType = mimeType; 475 mFileType = 0; 476 mFileSize = fileSize; 477 478 if (!isDirectory) { 479 if (!noMedia && isNoMediaFile(path)) { 480 noMedia = true; 481 } 482 mNoMedia = noMedia; 483 484 // try mimeType first, if it is specified 485 if (mimeType != null) { 486 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 487 } 488 489 // if mimeType was not specified, compute file type based on file extension. 490 if (mFileType == 0) { 491 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 492 if (mediaFileType != null) { 493 mFileType = mediaFileType.fileType; 494 if (mMimeType == null) { 495 mMimeType = mediaFileType.mimeType; 496 } 497 } 498 } 499 500 if (isDrmEnabled() && MediaFile.isDrmFileType(mFileType)) { 501 mFileType = getFileTypeFromDrm(path); 502 } 503 } 504 505 String key = path; 506 if (mCaseInsensitivePaths) { 507 key = path.toLowerCase(); 508 } 509 FileCacheEntry entry = mFileCache.get(key); 510 // add some slack to avoid a rounding error 511 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0; 512 boolean wasModified = delta > 1 || delta < -1; 513 if (entry == null || wasModified) { 514 if (wasModified) { 515 entry.mLastModified = lastModified; 516 } else { 517 entry = new FileCacheEntry(0, path, lastModified, 518 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0)); 519 mFileCache.put(key, entry); 520 } 521 entry.mLastModifiedChanged = true; 522 } 523 entry.mSeenInFileSystem = true; 524 525 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 526 mPlayLists.add(entry); 527 // we don't process playlists in the main scan, so return null 528 return null; 529 } 530 531 // clear all the metadata 532 mArtist = null; 533 mAlbumArtist = null; 534 mAlbum = null; 535 mTitle = null; 536 mComposer = null; 537 mGenre = null; 538 mTrack = 0; 539 mYear = 0; 540 mDuration = 0; 541 mPath = path; 542 mLastModified = lastModified; 543 mWriter = null; 544 mCompilation = 0; 545 mIsDrm = false; 546 547 return entry; 548 } 549 550 public void scanFile(String path, long lastModified, long fileSize, 551 boolean isDirectory, boolean noMedia) { 552 // This is the callback funtion from native codes. 553 // Log.v(TAG, "scanFile: "+path); 554 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia); 555 } 556 557 public Uri doScanFile(String path, String mimeType, long lastModified, 558 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 559 Uri result = null; 560// long t1 = System.currentTimeMillis(); 561 try { 562 FileCacheEntry entry = beginFile(path, mimeType, lastModified, 563 fileSize, isDirectory, noMedia); 564 // rescan for metadata if file was modified since last scan 565 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 566 if (noMedia) { 567 result = endFile(entry, false, false, false, false, false); 568 } else { 569 String lowpath = path.toLowerCase(); 570 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 571 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 572 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 573 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 574 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 575 (!ringtones && !notifications && !alarms && !podcasts); 576 577 // we only extract metadata for audio and video files 578 if (MediaFile.isAudioFileType(mFileType) 579 || MediaFile.isVideoFileType(mFileType)) { 580 processFile(path, mimeType, this); 581 } 582 583 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 584 } 585 } 586 } catch (RemoteException e) { 587 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 588 } 589// long t2 = System.currentTimeMillis(); 590// Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 591 return result; 592 } 593 594 private int parseSubstring(String s, int start, int defaultValue) { 595 int length = s.length(); 596 if (start == length) return defaultValue; 597 598 char ch = s.charAt(start++); 599 // return defaultValue if we have no integer at all 600 if (ch < '0' || ch > '9') return defaultValue; 601 602 int result = ch - '0'; 603 while (start < length) { 604 ch = s.charAt(start++); 605 if (ch < '0' || ch > '9') return result; 606 result = result * 10 + (ch - '0'); 607 } 608 609 return result; 610 } 611 612 public void handleStringTag(String name, String value) { 613 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 614 // Don't trim() here, to preserve the special \001 character 615 // used to force sorting. The media provider will trim() before 616 // inserting the title in to the database. 617 mTitle = value; 618 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 619 mArtist = value.trim(); 620 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") 621 || name.equalsIgnoreCase("band") || name.startsWith("band;")) { 622 mAlbumArtist = value.trim(); 623 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 624 mAlbum = value.trim(); 625 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 626 mComposer = value.trim(); 627 } else if (mProcessGenres && 628 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { 629 mGenre = getGenreName(value); 630 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 631 mYear = parseSubstring(value, 0, 0); 632 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 633 // track number might be of the form "2/12" 634 // we just read the number before the slash 635 int num = parseSubstring(value, 0, 0); 636 mTrack = (mTrack / 1000) * 1000 + num; 637 } else if (name.equalsIgnoreCase("discnumber") || 638 name.equals("set") || name.startsWith("set;")) { 639 // set number might be of the form "1/3" 640 // we just read the number before the slash 641 int num = parseSubstring(value, 0, 0); 642 mTrack = (num * 1000) + (mTrack % 1000); 643 } else if (name.equalsIgnoreCase("duration")) { 644 mDuration = parseSubstring(value, 0, 0); 645 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) { 646 mWriter = value.trim(); 647 } else if (name.equalsIgnoreCase("compilation")) { 648 mCompilation = parseSubstring(value, 0, 0); 649 } else if (name.equalsIgnoreCase("isdrm")) { 650 mIsDrm = (parseSubstring(value, 0, 0) == 1); 651 } 652 } 653 654 public String getGenreName(String genreTagValue) { 655 656 if (genreTagValue == null) { 657 return null; 658 } 659 final int length = genreTagValue.length(); 660 661 if (length > 0 && genreTagValue.charAt(0) == '(') { 662 StringBuffer number = new StringBuffer(); 663 int i = 1; 664 for (; i < length - 1; ++i) { 665 char c = genreTagValue.charAt(i); 666 if (Character.isDigit(c)) { 667 number.append(c); 668 } else { 669 break; 670 } 671 } 672 if (genreTagValue.charAt(i) == ')') { 673 try { 674 short genreIndex = Short.parseShort(number.toString()); 675 if (genreIndex >= 0) { 676 if (genreIndex < ID3_GENRES.length) { 677 return ID3_GENRES[genreIndex]; 678 } else if (genreIndex == 0xFF) { 679 return null; 680 } else if (genreIndex < 0xFF && (i + 1) < length) { 681 // genre is valid but unknown, 682 // if there is a string after the value we take it 683 return genreTagValue.substring(i + 1); 684 } else { 685 // else return the number, without parentheses 686 return number.toString(); 687 } 688 } 689 } catch (NumberFormatException e) { 690 } 691 } 692 } 693 694 return genreTagValue; 695 } 696 697 public void setMimeType(String mimeType) { 698 if ("audio/mp4".equals(mMimeType) && 699 mimeType.startsWith("video")) { 700 // for feature parity with Donut, we force m4a files to keep the 701 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 702 // with a video track 703 return; 704 } 705 mMimeType = mimeType; 706 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 707 } 708 709 /** 710 * Formats the data into a values array suitable for use with the Media 711 * Content Provider. 712 * 713 * @return a map of values 714 */ 715 private ContentValues toValues() { 716 ContentValues map = new ContentValues(); 717 718 map.put(MediaStore.MediaColumns.DATA, mPath); 719 map.put(MediaStore.MediaColumns.TITLE, mTitle); 720 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 721 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 722 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 723 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm); 724 725 if (!mNoMedia) { 726 if (MediaFile.isVideoFileType(mFileType)) { 727 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 728 ? mArtist : MediaStore.UNKNOWN_STRING)); 729 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 730 ? mAlbum : MediaStore.UNKNOWN_STRING)); 731 map.put(Video.Media.DURATION, mDuration); 732 // FIXME - add RESOLUTION 733 } else if (MediaFile.isImageFileType(mFileType)) { 734 // FIXME - add DESCRIPTION 735 } else if (MediaFile.isAudioFileType(mFileType)) { 736 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 737 mArtist : MediaStore.UNKNOWN_STRING); 738 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 739 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 740 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 741 mAlbum : MediaStore.UNKNOWN_STRING); 742 map.put(Audio.Media.COMPOSER, mComposer); 743 map.put(Audio.Media.GENRE, mGenre); 744 if (mYear != 0) { 745 map.put(Audio.Media.YEAR, mYear); 746 } 747 map.put(Audio.Media.TRACK, mTrack); 748 map.put(Audio.Media.DURATION, mDuration); 749 map.put(Audio.Media.COMPILATION, mCompilation); 750 } 751 } 752 return map; 753 } 754 755 private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, 756 boolean alarms, boolean music, boolean podcasts) 757 throws RemoteException { 758 // update database 759 760 // use album artist if artist is missing 761 if (mArtist == null || mArtist.length() == 0) { 762 mArtist = mAlbumArtist; 763 } 764 765 ContentValues values = toValues(); 766 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 767 if (title == null || TextUtils.isEmpty(title.trim())) { 768 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 769 values.put(MediaStore.MediaColumns.TITLE, title); 770 } 771 String album = values.getAsString(Audio.Media.ALBUM); 772 if (MediaStore.UNKNOWN_STRING.equals(album)) { 773 album = values.getAsString(MediaStore.MediaColumns.DATA); 774 // extract last path segment before file name 775 int lastSlash = album.lastIndexOf('/'); 776 if (lastSlash >= 0) { 777 int previousSlash = 0; 778 while (true) { 779 int idx = album.indexOf('/', previousSlash + 1); 780 if (idx < 0 || idx >= lastSlash) { 781 break; 782 } 783 previousSlash = idx; 784 } 785 if (previousSlash != 0) { 786 album = album.substring(previousSlash + 1, lastSlash); 787 values.put(Audio.Media.ALBUM, album); 788 } 789 } 790 } 791 long rowId = entry.mRowId; 792 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) { 793 // Only set these for new entries. For existing entries, they 794 // may have been modified later, and we want to keep the current 795 // values so that custom ringtones still show up in the ringtone 796 // picker. 797 values.put(Audio.Media.IS_RINGTONE, ringtones); 798 values.put(Audio.Media.IS_NOTIFICATION, notifications); 799 values.put(Audio.Media.IS_ALARM, alarms); 800 values.put(Audio.Media.IS_MUSIC, music); 801 values.put(Audio.Media.IS_PODCAST, podcasts); 802 } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) { 803 ExifInterface exif = null; 804 try { 805 exif = new ExifInterface(entry.mPath); 806 } catch (IOException ex) { 807 // exif is null 808 } 809 if (exif != null) { 810 float[] latlng = new float[2]; 811 if (exif.getLatLong(latlng)) { 812 values.put(Images.Media.LATITUDE, latlng[0]); 813 values.put(Images.Media.LONGITUDE, latlng[1]); 814 } 815 816 long time = exif.getGpsDateTime(); 817 if (time != -1) { 818 values.put(Images.Media.DATE_TAKEN, time); 819 } else { 820 // If no time zone information is available, we should consider using 821 // EXIF local time as taken time if the difference between file time 822 // and EXIF local time is not less than 1 Day, otherwise MediaProvider 823 // will use file time as taken time. 824 time = exif.getDateTime(); 825 if (Math.abs(mLastModified * 1000 - time) >= 86400000) { 826 values.put(Images.Media.DATE_TAKEN, time); 827 } 828 } 829 830 int orientation = exif.getAttributeInt( 831 ExifInterface.TAG_ORIENTATION, -1); 832 if (orientation != -1) { 833 // We only recognize a subset of orientation tag values. 834 int degree; 835 switch(orientation) { 836 case ExifInterface.ORIENTATION_ROTATE_90: 837 degree = 90; 838 break; 839 case ExifInterface.ORIENTATION_ROTATE_180: 840 degree = 180; 841 break; 842 case ExifInterface.ORIENTATION_ROTATE_270: 843 degree = 270; 844 break; 845 default: 846 degree = 0; 847 break; 848 } 849 values.put(Images.Media.ORIENTATION, degree); 850 } 851 } 852 } 853 854 Uri tableUri = mFilesUri; 855 FileInserter inserter = mFileInserter; 856 if (!mNoMedia) { 857 if (MediaFile.isVideoFileType(mFileType)) { 858 tableUri = mVideoUri; 859 inserter = mVideoInserter; 860 } else if (MediaFile.isImageFileType(mFileType)) { 861 tableUri = mImagesUri; 862 inserter = mImageInserter; 863 } else if (MediaFile.isAudioFileType(mFileType)) { 864 tableUri = mAudioUri; 865 inserter = mAudioInserter; 866 } 867 } 868 Uri result = null; 869 if (rowId == 0) { 870 if (mMtpObjectHandle != 0) { 871 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle); 872 } 873 if (tableUri == mFilesUri) { 874 int format = entry.mFormat; 875 if (format == 0) { 876 format = MediaFile.getFormatCode(entry.mPath, mMimeType); 877 } 878 values.put(Files.FileColumns.FORMAT, format); 879 } 880 // new file, insert it 881 // We insert directories immediately to ensure they are in the database 882 // before the files they contain. 883 // Otherwise we can get duplicate directory entries in the database 884 // if one of the media FileInserters is flushed before the files table FileInserter 885 if (inserter == null || entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 886 result = mMediaProvider.insert(tableUri, values); 887 } else { 888 result = inserter.insert(values); 889 } 890 891 if (result != null) { 892 rowId = ContentUris.parseId(result); 893 entry.mRowId = rowId; 894 } 895 } else { 896 // updated file 897 result = ContentUris.withAppendedId(tableUri, rowId); 898 // path should never change, and we want to avoid replacing mixed cased paths 899 // with squashed lower case paths 900 values.remove(MediaStore.MediaColumns.DATA); 901 mMediaProvider.update(result, values, null, null); 902 } 903 904 if (notifications && !mDefaultNotificationSet) { 905 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 906 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 907 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 908 mDefaultNotificationSet = true; 909 } 910 } else if (ringtones && !mDefaultRingtoneSet) { 911 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 912 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 913 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 914 mDefaultRingtoneSet = true; 915 } 916 } else if (alarms && !mDefaultAlarmSet) { 917 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 918 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 919 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 920 mDefaultAlarmSet = true; 921 } 922 } 923 924 return result; 925 } 926 927 private boolean doesPathHaveFilename(String path, String filename) { 928 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 929 int filenameLength = filename.length(); 930 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 931 pathFilenameStart + filenameLength == path.length(); 932 } 933 934 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 935 936 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 937 settingName); 938 939 if (TextUtils.isEmpty(existingSettingValue)) { 940 // Set the setting to the given URI 941 Settings.System.putString(mContext.getContentResolver(), settingName, 942 ContentUris.withAppendedId(uri, rowId).toString()); 943 } 944 } 945 946 private int getFileTypeFromDrm(String path) { 947 if (!isDrmEnabled()) { 948 return 0; 949 } 950 951 int resultFileType = 0; 952 953 if (mDrmManagerClient == null) { 954 mDrmManagerClient = new DrmManagerClient(mContext); 955 } 956 957 if (mDrmManagerClient.canHandle(path, null)) { 958 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path); 959 if (drmMimetype != null) { 960 mMimeType = drmMimetype; 961 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype); 962 } 963 } 964 return resultFileType; 965 } 966 967 }; // end of anonymous MediaScannerClient instance 968 969 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 970 Cursor c = null; 971 String where = null; 972 String[] selectionArgs = null; 973 974 if (mFileCache == null) { 975 mFileCache = new HashMap<String, FileCacheEntry>(); 976 } else { 977 mFileCache.clear(); 978 } 979 if (mPlayLists == null) { 980 mPlayLists = new ArrayList<FileCacheEntry>(); 981 } else { 982 mPlayLists.clear(); 983 } 984 985 if (filePath != null) { 986 // query for only one file 987 where = Files.FileColumns.DATA + "=?"; 988 selectionArgs = new String[] { filePath }; 989 } 990 991 // Build the list of files from the content provider 992 try { 993 if (prescanFiles) { 994 // First read existing files from the files table 995 996 c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 997 where, selectionArgs, null); 998 999 if (c != null) { 1000 while (c.moveToNext()) { 1001 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1002 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1003 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1004 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1005 1006 // Only consider entries with absolute path names. 1007 // This allows storing URIs in the database without the 1008 // media scanner removing them. 1009 if (path != null && path.startsWith("/")) { 1010 String key = path; 1011 if (mCaseInsensitivePaths) { 1012 key = path.toLowerCase(); 1013 } 1014 1015 FileCacheEntry entry = new FileCacheEntry(rowId, path, 1016 lastModified, format); 1017 mFileCache.put(key, entry); 1018 } 1019 } 1020 c.close(); 1021 c = null; 1022 } 1023 } 1024 } 1025 finally { 1026 if (c != null) { 1027 c.close(); 1028 } 1029 } 1030 1031 // compute original size of images 1032 mOriginalCount = 0; 1033 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null); 1034 if (c != null) { 1035 mOriginalCount = c.getCount(); 1036 c.close(); 1037 } 1038 } 1039 1040 private boolean inScanDirectory(String path, String[] directories) { 1041 for (int i = 0; i < directories.length; i++) { 1042 String directory = directories[i]; 1043 if (path.startsWith(directory)) { 1044 return true; 1045 } 1046 } 1047 return false; 1048 } 1049 1050 private void pruneDeadThumbnailFiles() { 1051 HashSet<String> existingFiles = new HashSet<String>(); 1052 String directory = "/sdcard/DCIM/.thumbnails"; 1053 String [] files = (new File(directory)).list(); 1054 if (files == null) 1055 files = new String[0]; 1056 1057 for (int i = 0; i < files.length; i++) { 1058 String fullPathString = directory + "/" + files[i]; 1059 existingFiles.add(fullPathString); 1060 } 1061 1062 try { 1063 Cursor c = mMediaProvider.query( 1064 mThumbsUri, 1065 new String [] { "_data" }, 1066 null, 1067 null, 1068 null); 1069 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1070 if (c != null && c.moveToFirst()) { 1071 do { 1072 String fullPathString = c.getString(0); 1073 existingFiles.remove(fullPathString); 1074 } while (c.moveToNext()); 1075 } 1076 1077 for (String fileToDelete : existingFiles) { 1078 if (false) 1079 Log.v(TAG, "fileToDelete is " + fileToDelete); 1080 try { 1081 (new File(fileToDelete)).delete(); 1082 } catch (SecurityException ex) { 1083 } 1084 } 1085 1086 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1087 if (c != null) { 1088 c.close(); 1089 } 1090 } catch (RemoteException e) { 1091 // We will soon be killed... 1092 } 1093 } 1094 1095 private void postscan(String[] directories) throws RemoteException { 1096 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1097 1098 while (iterator.hasNext()) { 1099 FileCacheEntry entry = iterator.next(); 1100 String path = entry.mPath; 1101 1102 // remove database entries for files that no longer exist. 1103 boolean fileMissing = false; 1104 1105 if (!entry.mSeenInFileSystem && !MtpConstants.isAbstractObject(entry.mFormat)) { 1106 if (inScanDirectory(path, directories)) { 1107 // we didn't see this file in the scan directory. 1108 fileMissing = true; 1109 } else { 1110 // the file actually a directory or other abstract object 1111 // or is outside of our scan directory, 1112 // so we need to check for file existence here. 1113 File testFile = new File(path); 1114 if (!testFile.exists()) { 1115 fileMissing = true; 1116 } 1117 } 1118 } 1119 1120 if (fileMissing) { 1121 // do not delete missing playlists, since they may have been modified by the user. 1122 // the user can delete them in the media player instead. 1123 // instead, clear the path and lastModified fields in the row 1124 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1125 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1126 1127 if (MediaFile.isPlayListFileType(fileType)) { 1128 ContentValues values = new ContentValues(); 1129 values.put(MediaStore.Audio.Playlists.DATA, ""); 1130 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 1131 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), 1132 values, null, null); 1133 } else { 1134 mMediaProvider.delete(ContentUris.withAppendedId(mFilesUri, entry.mRowId), 1135 null, null); 1136 iterator.remove(); 1137 } 1138 } 1139 } 1140 1141 // handle playlists last, after we know what media files are on the storage. 1142 if (mProcessPlaylists) { 1143 processPlayLists(); 1144 } 1145 1146 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1147 pruneDeadThumbnailFiles(); 1148 1149 // allow GC to clean up 1150 mPlayLists = null; 1151 mFileCache = null; 1152 mMediaProvider = null; 1153 } 1154 1155 private void initialize(String volumeName) { 1156 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1157 1158 mAudioUri = Audio.Media.getContentUri(volumeName); 1159 mVideoUri = Video.Media.getContentUri(volumeName); 1160 mImagesUri = Images.Media.getContentUri(volumeName); 1161 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1162 mFilesUri = Files.getContentUri(volumeName); 1163 1164 if (!volumeName.equals("internal")) { 1165 // we only support playlists on external media 1166 mProcessPlaylists = true; 1167 mProcessGenres = true; 1168 mPlaylistsUri = Playlists.getContentUri(volumeName); 1169 1170 mCaseInsensitivePaths = true; 1171 } 1172 } 1173 1174 public void scanDirectories(String[] directories, String volumeName) { 1175 try { 1176 long start = System.currentTimeMillis(); 1177 initialize(volumeName); 1178 prescan(null, true); 1179 long prescan = System.currentTimeMillis(); 1180 1181 if (ENABLE_BULK_INSERTS) { 1182 // create FileInserters for bulk inserts 1183 mAudioInserter = new FileInserter(mAudioUri, 500); 1184 mVideoInserter = new FileInserter(mVideoUri, 500); 1185 mImageInserter = new FileInserter(mImagesUri, 500); 1186 mFileInserter = new FileInserter(mFilesUri, 500); 1187 } 1188 1189 for (int i = 0; i < directories.length; i++) { 1190 processDirectory(directories[i], mClient); 1191 } 1192 1193 if (ENABLE_BULK_INSERTS) { 1194 // flush remaining inserts 1195 mAudioInserter.flush(); 1196 mVideoInserter.flush(); 1197 mImageInserter.flush(); 1198 mFileInserter.flush(); 1199 mAudioInserter = null; 1200 mVideoInserter = null; 1201 mImageInserter = null; 1202 mFileInserter = null; 1203 } 1204 1205 long scan = System.currentTimeMillis(); 1206 postscan(directories); 1207 long end = System.currentTimeMillis(); 1208 1209 if (false) { 1210 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1211 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1212 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1213 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1214 } 1215 } catch (SQLException e) { 1216 // this might happen if the SD card is removed while the media scanner is running 1217 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1218 } catch (UnsupportedOperationException e) { 1219 // this might happen if the SD card is removed while the media scanner is running 1220 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1221 } catch (RemoteException e) { 1222 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1223 } 1224 } 1225 1226 // this function is used to scan a single file 1227 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1228 try { 1229 initialize(volumeName); 1230 prescan(path, true); 1231 1232 File file = new File(path); 1233 1234 // lastModified is in milliseconds on Files. 1235 long lastModifiedSeconds = file.lastModified() / 1000; 1236 1237 // always scan the file, so we can return the content://media Uri for existing files 1238 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1239 false, true, false); 1240 } catch (RemoteException e) { 1241 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1242 return null; 1243 } 1244 } 1245 1246 private static boolean isNoMediaFile(String path) { 1247 File file = new File(path); 1248 if (file.isDirectory()) return false; 1249 1250 // special case certain file names 1251 // I use regionMatches() instead of substring() below 1252 // to avoid memory allocation 1253 int lastSlash = path.lastIndexOf('/'); 1254 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1255 // ignore those ._* files created by MacOS 1256 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1257 return true; 1258 } 1259 1260 // ignore album art files created by Windows Media Player: 1261 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1262 // and AlbumArt_{...}_Small.jpg 1263 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1264 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1265 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1266 return true; 1267 } 1268 int length = path.length() - lastSlash - 1; 1269 if ((length == 17 && path.regionMatches( 1270 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1271 (length == 10 1272 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1273 return true; 1274 } 1275 } 1276 } 1277 return false; 1278 } 1279 1280 public static boolean isNoMediaPath(String path) { 1281 if (path == null) return false; 1282 1283 // return true if file or any parent directory has name starting with a dot 1284 if (path.indexOf("/.") >= 0) return true; 1285 1286 // now check to see if any parent directories have a ".nomedia" file 1287 // start from 1 so we don't bother checking in the root directory 1288 int offset = 1; 1289 while (offset >= 0) { 1290 int slashIndex = path.indexOf('/', offset); 1291 if (slashIndex > offset) { 1292 slashIndex++; // move past slash 1293 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1294 if (file.exists()) { 1295 // we have a .nomedia in one of the parent directories 1296 return true; 1297 } 1298 } 1299 offset = slashIndex; 1300 } 1301 return isNoMediaFile(path); 1302 } 1303 1304 public void scanMtpFile(String path, String volumeName, int objectHandle, int format) { 1305 initialize(volumeName); 1306 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1307 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1308 File file = new File(path); 1309 long lastModifiedSeconds = file.lastModified() / 1000; 1310 1311 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1312 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType)) { 1313 1314 // no need to use the media scanner, but we need to update last modified and file size 1315 ContentValues values = new ContentValues(); 1316 values.put(Files.FileColumns.SIZE, file.length()); 1317 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1318 try { 1319 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1320 mMediaProvider.update(Files.getMtpObjectsUri(volumeName), values, "_id=?", 1321 whereArgs); 1322 } catch (RemoteException e) { 1323 Log.e(TAG, "RemoteException in scanMtpFile", e); 1324 } 1325 return; 1326 } 1327 1328 mMtpObjectHandle = objectHandle; 1329 try { 1330 if (MediaFile.isPlayListFileType(fileType)) { 1331 // build file cache so we can look up tracks in the playlist 1332 prescan(null, true); 1333 1334 String key = path; 1335 if (mCaseInsensitivePaths) { 1336 key = path.toLowerCase(); 1337 } 1338 FileCacheEntry entry = mFileCache.get(key); 1339 if (entry != null) { 1340 processPlayList(entry); 1341 } 1342 } else { 1343 // MTP will create a file entry for us so we don't want to do it in prescan 1344 prescan(path, false); 1345 1346 // always scan the file, so we can return the content://media Uri for existing files 1347 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1348 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1349 } 1350 } catch (RemoteException e) { 1351 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1352 } finally { 1353 mMtpObjectHandle = 0; 1354 } 1355 } 1356 1357 // returns the number of matching file/directory names, starting from the right 1358 private int matchPaths(String path1, String path2) { 1359 int result = 0; 1360 int end1 = path1.length(); 1361 int end2 = path2.length(); 1362 1363 while (end1 > 0 && end2 > 0) { 1364 int slash1 = path1.lastIndexOf('/', end1 - 1); 1365 int slash2 = path2.lastIndexOf('/', end2 - 1); 1366 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1367 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1368 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1369 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1370 if (start1 < 0) start1 = 0; else start1++; 1371 if (start2 < 0) start2 = 0; else start2++; 1372 int length = end1 - start1; 1373 if (end2 - start2 != length) break; 1374 if (path1.regionMatches(true, start1, path2, start2, length)) { 1375 result++; 1376 end1 = start1 - 1; 1377 end2 = start2 - 1; 1378 } else break; 1379 } 1380 1381 return result; 1382 } 1383 1384 private boolean addPlayListEntry(String entry, String playListDirectory, 1385 Uri uri, ContentValues values, int index) { 1386 1387 // watch for trailing whitespace 1388 int entryLength = entry.length(); 1389 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1390 // path should be longer than 3 characters. 1391 // avoid index out of bounds errors below by returning here. 1392 if (entryLength < 3) return false; 1393 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1394 1395 // does entry appear to be an absolute path? 1396 // look for Unix or DOS absolute paths 1397 char ch1 = entry.charAt(0); 1398 boolean fullPath = (ch1 == '/' || 1399 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1400 // if we have a relative path, combine entry with playListDirectory 1401 if (!fullPath) 1402 entry = playListDirectory + entry; 1403 1404 //FIXME - should we look for "../" within the path? 1405 1406 // best matching MediaFile for the play list entry 1407 FileCacheEntry bestMatch = null; 1408 1409 // number of rightmost file/directory names for bestMatch 1410 int bestMatchLength = 0; 1411 1412 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1413 while (iterator.hasNext()) { 1414 FileCacheEntry cacheEntry = iterator.next(); 1415 String path = cacheEntry.mPath; 1416 1417 if (path.equalsIgnoreCase(entry)) { 1418 bestMatch = cacheEntry; 1419 break; // don't bother continuing search 1420 } 1421 1422 int matchLength = matchPaths(path, entry); 1423 if (matchLength > bestMatchLength) { 1424 bestMatch = cacheEntry; 1425 bestMatchLength = matchLength; 1426 } 1427 } 1428 1429 if (bestMatch == null) { 1430 return false; 1431 } 1432 1433 try { 1434 // OK, now we need to add this to the database 1435 values.clear(); 1436 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1437 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1438 mMediaProvider.insert(uri, values); 1439 } catch (RemoteException e) { 1440 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1441 return false; 1442 } 1443 1444 return true; 1445 } 1446 1447 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1448 BufferedReader reader = null; 1449 try { 1450 File f = new File(path); 1451 if (f.exists()) { 1452 reader = new BufferedReader( 1453 new InputStreamReader(new FileInputStream(f)), 8192); 1454 String line = reader.readLine(); 1455 int index = 0; 1456 while (line != null) { 1457 // ignore comment lines, which begin with '#' 1458 if (line.length() > 0 && line.charAt(0) != '#') { 1459 values.clear(); 1460 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1461 index++; 1462 } 1463 line = reader.readLine(); 1464 } 1465 } 1466 } catch (IOException e) { 1467 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1468 } finally { 1469 try { 1470 if (reader != null) 1471 reader.close(); 1472 } catch (IOException e) { 1473 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1474 } 1475 } 1476 } 1477 1478 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1479 BufferedReader reader = null; 1480 try { 1481 File f = new File(path); 1482 if (f.exists()) { 1483 reader = new BufferedReader( 1484 new InputStreamReader(new FileInputStream(f)), 8192); 1485 String line = reader.readLine(); 1486 int index = 0; 1487 while (line != null) { 1488 // ignore comment lines, which begin with '#' 1489 if (line.startsWith("File")) { 1490 int equals = line.indexOf('='); 1491 if (equals > 0) { 1492 values.clear(); 1493 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1494 index++; 1495 } 1496 } 1497 line = reader.readLine(); 1498 } 1499 } 1500 } catch (IOException e) { 1501 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1502 } finally { 1503 try { 1504 if (reader != null) 1505 reader.close(); 1506 } catch (IOException e) { 1507 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1508 } 1509 } 1510 } 1511 1512 class WplHandler implements ElementListener { 1513 1514 final ContentHandler handler; 1515 String playListDirectory; 1516 Uri uri; 1517 ContentValues values = new ContentValues(); 1518 int index = 0; 1519 1520 public WplHandler(String playListDirectory, Uri uri) { 1521 this.playListDirectory = playListDirectory; 1522 this.uri = uri; 1523 1524 RootElement root = new RootElement("smil"); 1525 Element body = root.getChild("body"); 1526 Element seq = body.getChild("seq"); 1527 Element media = seq.getChild("media"); 1528 media.setElementListener(this); 1529 1530 this.handler = root.getContentHandler(); 1531 } 1532 1533 public void start(Attributes attributes) { 1534 String path = attributes.getValue("", "src"); 1535 if (path != null) { 1536 values.clear(); 1537 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1538 index++; 1539 } 1540 } 1541 } 1542 1543 public void end() { 1544 } 1545 1546 ContentHandler getContentHandler() { 1547 return handler; 1548 } 1549 } 1550 1551 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1552 FileInputStream fis = null; 1553 try { 1554 File f = new File(path); 1555 if (f.exists()) { 1556 fis = new FileInputStream(f); 1557 1558 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1559 } 1560 } catch (SAXException e) { 1561 e.printStackTrace(); 1562 } catch (IOException e) { 1563 e.printStackTrace(); 1564 } finally { 1565 try { 1566 if (fis != null) 1567 fis.close(); 1568 } catch (IOException e) { 1569 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1570 } 1571 } 1572 } 1573 1574 private void processPlayList(FileCacheEntry entry) throws RemoteException { 1575 String path = entry.mPath; 1576 ContentValues values = new ContentValues(); 1577 int lastSlash = path.lastIndexOf('/'); 1578 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1579 Uri uri, membersUri; 1580 long rowId = entry.mRowId; 1581 1582 // make sure we have a name 1583 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1584 if (name == null) { 1585 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1586 if (name == null) { 1587 // extract name from file name 1588 int lastDot = path.lastIndexOf('.'); 1589 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1590 : path.substring(lastSlash + 1, lastDot)); 1591 } 1592 } 1593 1594 values.put(MediaStore.Audio.Playlists.NAME, name); 1595 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1596 1597 if (rowId == 0) { 1598 values.put(MediaStore.Audio.Playlists.DATA, path); 1599 uri = mMediaProvider.insert(mPlaylistsUri, values); 1600 rowId = ContentUris.parseId(uri); 1601 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1602 } else { 1603 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1604 mMediaProvider.update(uri, values, null, null); 1605 1606 // delete members of existing playlist 1607 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1608 mMediaProvider.delete(membersUri, null, null); 1609 } 1610 1611 String playListDirectory = path.substring(0, lastSlash + 1); 1612 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1613 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1614 1615 if (fileType == MediaFile.FILE_TYPE_M3U) { 1616 processM3uPlayList(path, playListDirectory, membersUri, values); 1617 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1618 processPlsPlayList(path, playListDirectory, membersUri, values); 1619 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1620 processWplPlayList(path, playListDirectory, membersUri); 1621 } 1622 } 1623 1624 private void processPlayLists() throws RemoteException { 1625 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1626 while (iterator.hasNext()) { 1627 FileCacheEntry entry = iterator.next(); 1628 // only process playlist files if they are new or have been modified since the last scan 1629 if (entry.mLastModifiedChanged) { 1630 processPlayList(entry); 1631 } 1632 } 1633 } 1634 1635 private native void processDirectory(String path, MediaScannerClient client); 1636 private native void processFile(String path, String mimeType, MediaScannerClient client); 1637 public native void setLocale(String locale); 1638 1639 public native byte[] extractAlbumArt(FileDescriptor fd); 1640 1641 private static native final void native_init(); 1642 private native final void native_setup(); 1643 private native final void native_finalize(); 1644 1645 /** 1646 * Releases resouces associated with this MediaScanner object. 1647 * It is considered good practice to call this method when 1648 * one is done using the MediaScanner object. After this method 1649 * is called, the MediaScanner object can no longer be used. 1650 */ 1651 public void release() { 1652 native_finalize(); 1653 } 1654 1655 @Override 1656 protected void finalize() { 1657 mContext.getContentResolver().releaseProvider(mMediaProvider); 1658 native_finalize(); 1659 } 1660} 1661