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