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