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