MediaScanner.java revision 8ad76fa7a644c10abb7b95398fdb8a84bd723619
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 "Negerpunk", 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) { 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 result = mMediaProvider.insert(tableUri, values); 962 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 963 inserter.insertwithPriority(tableUri, values); 964 } else { 965 inserter.insert(tableUri, values); 966 } 967 968 if (result != null) { 969 rowId = ContentUris.parseId(result); 970 entry.mRowId = rowId; 971 } 972 } else { 973 // updated file 974 result = ContentUris.withAppendedId(tableUri, rowId); 975 // path should never change, and we want to avoid replacing mixed cased paths 976 // with squashed lower case paths 977 values.remove(MediaStore.MediaColumns.DATA); 978 979 int mediaType = 0; 980 if (!MediaScanner.isNoMediaPath(entry.mPath)) { 981 int fileType = MediaFile.getFileTypeForMimeType(mMimeType); 982 if (MediaFile.isAudioFileType(fileType)) { 983 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 984 } else if (MediaFile.isVideoFileType(fileType)) { 985 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 986 } else if (MediaFile.isImageFileType(fileType)) { 987 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 988 } else if (MediaFile.isPlayListFileType(fileType)) { 989 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 990 } 991 values.put(FileColumns.MEDIA_TYPE, mediaType); 992 } 993 mMediaProvider.update(result, values, null, null); 994 } 995 996 if(needToSetSettings) { 997 if (notifications) { 998 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 999 mDefaultNotificationSet = true; 1000 } else if (ringtones) { 1001 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 1002 mDefaultRingtoneSet = true; 1003 } else if (alarms) { 1004 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 1005 mDefaultAlarmSet = true; 1006 } 1007 } 1008 1009 return result; 1010 } 1011 1012 private boolean doesPathHaveFilename(String path, String filename) { 1013 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 1014 int filenameLength = filename.length(); 1015 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 1016 pathFilenameStart + filenameLength == path.length(); 1017 } 1018 1019 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 1020 1021 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 1022 settingName); 1023 1024 if (TextUtils.isEmpty(existingSettingValue)) { 1025 // Set the setting to the given URI 1026 Settings.System.putString(mContext.getContentResolver(), settingName, 1027 ContentUris.withAppendedId(uri, rowId).toString()); 1028 } 1029 } 1030 1031 private int getFileTypeFromDrm(String path) { 1032 if (!isDrmEnabled()) { 1033 return 0; 1034 } 1035 1036 int resultFileType = 0; 1037 1038 if (mDrmManagerClient == null) { 1039 mDrmManagerClient = new DrmManagerClient(mContext); 1040 } 1041 1042 if (mDrmManagerClient.canHandle(path, null)) { 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 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); 1280 1281 if (!volumeName.equals("internal")) { 1282 // we only support playlists on external media 1283 mProcessPlaylists = true; 1284 mProcessGenres = true; 1285 mPlaylistsUri = Playlists.getContentUri(volumeName); 1286 1287 mCaseInsensitivePaths = true; 1288 } 1289 } 1290 1291 public void scanDirectories(String[] directories, String volumeName) { 1292 try { 1293 long start = System.currentTimeMillis(); 1294 initialize(volumeName); 1295 prescan(null, true); 1296 long prescan = System.currentTimeMillis(); 1297 1298 if (ENABLE_BULK_INSERTS) { 1299 // create MediaInserter for bulk inserts 1300 mMediaInserter = new MediaInserter(mMediaProvider, 500); 1301 } 1302 1303 for (int i = 0; i < directories.length; i++) { 1304 processDirectory(directories[i], mClient); 1305 } 1306 1307 if (ENABLE_BULK_INSERTS) { 1308 // flush remaining inserts 1309 mMediaInserter.flushAll(); 1310 mMediaInserter = null; 1311 } 1312 1313 long scan = System.currentTimeMillis(); 1314 postscan(directories); 1315 long end = System.currentTimeMillis(); 1316 1317 if (false) { 1318 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1319 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1320 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1321 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1322 } 1323 } catch (SQLException e) { 1324 // this might happen if the SD card is removed while the media scanner is running 1325 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1326 } catch (UnsupportedOperationException e) { 1327 // this might happen if the SD card is removed while the media scanner is running 1328 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1329 } catch (RemoteException e) { 1330 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1331 } 1332 } 1333 1334 // this function is used to scan a single file 1335 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1336 try { 1337 initialize(volumeName); 1338 prescan(path, true); 1339 1340 File file = new File(path); 1341 if (!file.exists()) { 1342 return null; 1343 } 1344 1345 // lastModified is in milliseconds on Files. 1346 long lastModifiedSeconds = file.lastModified() / 1000; 1347 1348 // always scan the file, so we can return the content://media Uri for existing files 1349 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1350 false, true, false); 1351 } catch (RemoteException e) { 1352 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1353 return null; 1354 } 1355 } 1356 1357 private static boolean isNoMediaFile(String path) { 1358 File file = new File(path); 1359 if (file.isDirectory()) return false; 1360 1361 // special case certain file names 1362 // I use regionMatches() instead of substring() below 1363 // to avoid memory allocation 1364 int lastSlash = path.lastIndexOf('/'); 1365 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1366 // ignore those ._* files created by MacOS 1367 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1368 return true; 1369 } 1370 1371 // ignore album art files created by Windows Media Player: 1372 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1373 // and AlbumArt_{...}_Small.jpg 1374 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1375 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1376 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1377 return true; 1378 } 1379 int length = path.length() - lastSlash - 1; 1380 if ((length == 17 && path.regionMatches( 1381 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1382 (length == 10 1383 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1384 return true; 1385 } 1386 } 1387 } 1388 return false; 1389 } 1390 1391 public static boolean isNoMediaPath(String path) { 1392 if (path == null) return false; 1393 1394 // return true if file or any parent directory has name starting with a dot 1395 if (path.indexOf("/.") >= 0) return true; 1396 1397 // now check to see if any parent directories have a ".nomedia" file 1398 // start from 1 so we don't bother checking in the root directory 1399 int offset = 1; 1400 while (offset >= 0) { 1401 int slashIndex = path.indexOf('/', offset); 1402 if (slashIndex > offset) { 1403 slashIndex++; // move past slash 1404 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1405 if (file.exists()) { 1406 // we have a .nomedia in one of the parent directories 1407 return true; 1408 } 1409 } 1410 offset = slashIndex; 1411 } 1412 return isNoMediaFile(path); 1413 } 1414 1415 public void scanMtpFile(String path, String volumeName, int objectHandle, int format) { 1416 initialize(volumeName); 1417 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1418 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1419 File file = new File(path); 1420 long lastModifiedSeconds = file.lastModified() / 1000; 1421 1422 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1423 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) && 1424 !MediaFile.isDrmFileType(fileType)) { 1425 1426 // no need to use the media scanner, but we need to update last modified and file size 1427 ContentValues values = new ContentValues(); 1428 values.put(Files.FileColumns.SIZE, file.length()); 1429 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1430 try { 1431 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1432 mMediaProvider.update(Files.getMtpObjectsUri(volumeName), values, "_id=?", 1433 whereArgs); 1434 } catch (RemoteException e) { 1435 Log.e(TAG, "RemoteException in scanMtpFile", e); 1436 } 1437 return; 1438 } 1439 1440 mMtpObjectHandle = objectHandle; 1441 Cursor fileList = null; 1442 try { 1443 if (MediaFile.isPlayListFileType(fileType)) { 1444 // build file cache so we can look up tracks in the playlist 1445 prescan(null, true); 1446 1447 FileEntry entry = makeEntryFor(path); 1448 if (entry != null) { 1449 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1450 null, null, null, null); 1451 processPlayList(entry, fileList); 1452 } 1453 } else { 1454 // MTP will create a file entry for us so we don't want to do it in prescan 1455 prescan(path, false); 1456 1457 // always scan the file, so we can return the content://media Uri for existing files 1458 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1459 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1460 } 1461 } catch (RemoteException e) { 1462 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1463 } finally { 1464 mMtpObjectHandle = 0; 1465 if (fileList != null) { 1466 fileList.close(); 1467 } 1468 } 1469 } 1470 1471 FileEntry makeEntryFor(String path) { 1472 String where; 1473 String[] selectionArgs; 1474 1475 Cursor c = null; 1476 try { 1477 where = Files.FileColumns.DATA + "=?"; 1478 selectionArgs = new String[] { path }; 1479 c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION, 1480 where, selectionArgs, null, null); 1481 if (c.moveToFirst()) { 1482 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1483 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1484 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1485 return new FileEntry(rowId, path, lastModified, format); 1486 } 1487 } catch (RemoteException e) { 1488 } finally { 1489 if (c != null) { 1490 c.close(); 1491 } 1492 } 1493 return null; 1494 } 1495 1496 // returns the number of matching file/directory names, starting from the right 1497 private int matchPaths(String path1, String path2) { 1498 int result = 0; 1499 int end1 = path1.length(); 1500 int end2 = path2.length(); 1501 1502 while (end1 > 0 && end2 > 0) { 1503 int slash1 = path1.lastIndexOf('/', end1 - 1); 1504 int slash2 = path2.lastIndexOf('/', end2 - 1); 1505 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1506 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1507 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1508 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1509 if (start1 < 0) start1 = 0; else start1++; 1510 if (start2 < 0) start2 = 0; else start2++; 1511 int length = end1 - start1; 1512 if (end2 - start2 != length) break; 1513 if (path1.regionMatches(true, start1, path2, start2, length)) { 1514 result++; 1515 end1 = start1 - 1; 1516 end2 = start2 - 1; 1517 } else break; 1518 } 1519 1520 return result; 1521 } 1522 1523 private boolean matchEntries(long rowId, String data) { 1524 1525 int len = mPlaylistEntries.size(); 1526 boolean done = true; 1527 for (int i = 0; i < len; i++) { 1528 PlaylistEntry entry = mPlaylistEntries.get(i); 1529 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1530 continue; // this entry has been matched already 1531 } 1532 done = false; 1533 if (data.equalsIgnoreCase(entry.path)) { 1534 entry.bestmatchid = rowId; 1535 entry.bestmatchlevel = Integer.MAX_VALUE; 1536 continue; // no need for path matching 1537 } 1538 1539 int matchLength = matchPaths(data, entry.path); 1540 if (matchLength > entry.bestmatchlevel) { 1541 entry.bestmatchid = rowId; 1542 entry.bestmatchlevel = matchLength; 1543 } 1544 } 1545 return done; 1546 } 1547 1548 private void cachePlaylistEntry(String line, String playListDirectory) { 1549 PlaylistEntry entry = new PlaylistEntry(); 1550 // watch for trailing whitespace 1551 int entryLength = line.length(); 1552 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1553 // path should be longer than 3 characters. 1554 // avoid index out of bounds errors below by returning here. 1555 if (entryLength < 3) return; 1556 if (entryLength < line.length()) line = line.substring(0, entryLength); 1557 1558 // does entry appear to be an absolute path? 1559 // look for Unix or DOS absolute paths 1560 char ch1 = line.charAt(0); 1561 boolean fullPath = (ch1 == '/' || 1562 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1563 // if we have a relative path, combine entry with playListDirectory 1564 if (!fullPath) 1565 line = playListDirectory + line; 1566 entry.path = line; 1567 //FIXME - should we look for "../" within the path? 1568 1569 mPlaylistEntries.add(entry); 1570 } 1571 1572 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1573 fileList.moveToPosition(-1); 1574 while (fileList.moveToNext()) { 1575 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1576 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1577 if (matchEntries(rowId, data)) { 1578 break; 1579 } 1580 } 1581 1582 int len = mPlaylistEntries.size(); 1583 int index = 0; 1584 for (int i = 0; i < len; i++) { 1585 PlaylistEntry entry = mPlaylistEntries.get(i); 1586 if (entry.bestmatchlevel > 0) { 1587 try { 1588 values.clear(); 1589 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1590 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1591 mMediaProvider.insert(playlistUri, values); 1592 index++; 1593 } catch (RemoteException e) { 1594 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1595 return; 1596 } 1597 } 1598 } 1599 mPlaylistEntries.clear(); 1600 } 1601 1602 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1603 ContentValues values, Cursor fileList) { 1604 BufferedReader reader = null; 1605 try { 1606 File f = new File(path); 1607 if (f.exists()) { 1608 reader = new BufferedReader( 1609 new InputStreamReader(new FileInputStream(f)), 8192); 1610 String line = reader.readLine(); 1611 mPlaylistEntries.clear(); 1612 while (line != null) { 1613 // ignore comment lines, which begin with '#' 1614 if (line.length() > 0 && line.charAt(0) != '#') { 1615 cachePlaylistEntry(line, playListDirectory); 1616 } 1617 line = reader.readLine(); 1618 } 1619 1620 processCachedPlaylist(fileList, values, uri); 1621 } 1622 } catch (IOException e) { 1623 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1624 } finally { 1625 try { 1626 if (reader != null) 1627 reader.close(); 1628 } catch (IOException e) { 1629 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1630 } 1631 } 1632 } 1633 1634 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1635 ContentValues values, Cursor fileList) { 1636 BufferedReader reader = null; 1637 try { 1638 File f = new File(path); 1639 if (f.exists()) { 1640 reader = new BufferedReader( 1641 new InputStreamReader(new FileInputStream(f)), 8192); 1642 String line = reader.readLine(); 1643 mPlaylistEntries.clear(); 1644 while (line != null) { 1645 // ignore comment lines, which begin with '#' 1646 if (line.startsWith("File")) { 1647 int equals = line.indexOf('='); 1648 if (equals > 0) { 1649 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1650 } 1651 } 1652 line = reader.readLine(); 1653 } 1654 1655 processCachedPlaylist(fileList, values, uri); 1656 } 1657 } catch (IOException e) { 1658 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1659 } finally { 1660 try { 1661 if (reader != null) 1662 reader.close(); 1663 } catch (IOException e) { 1664 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1665 } 1666 } 1667 } 1668 1669 class WplHandler implements ElementListener { 1670 1671 final ContentHandler handler; 1672 String playListDirectory; 1673 1674 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1675 this.playListDirectory = playListDirectory; 1676 1677 RootElement root = new RootElement("smil"); 1678 Element body = root.getChild("body"); 1679 Element seq = body.getChild("seq"); 1680 Element media = seq.getChild("media"); 1681 media.setElementListener(this); 1682 1683 this.handler = root.getContentHandler(); 1684 } 1685 1686 @Override 1687 public void start(Attributes attributes) { 1688 String path = attributes.getValue("", "src"); 1689 if (path != null) { 1690 cachePlaylistEntry(path, playListDirectory); 1691 } 1692 } 1693 1694 @Override 1695 public void end() { 1696 } 1697 1698 ContentHandler getContentHandler() { 1699 return handler; 1700 } 1701 } 1702 1703 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1704 ContentValues values, Cursor fileList) { 1705 FileInputStream fis = null; 1706 try { 1707 File f = new File(path); 1708 if (f.exists()) { 1709 fis = new FileInputStream(f); 1710 1711 mPlaylistEntries.clear(); 1712 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1713 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1714 1715 processCachedPlaylist(fileList, values, uri); 1716 } 1717 } catch (SAXException e) { 1718 e.printStackTrace(); 1719 } catch (IOException e) { 1720 e.printStackTrace(); 1721 } finally { 1722 try { 1723 if (fis != null) 1724 fis.close(); 1725 } catch (IOException e) { 1726 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1727 } 1728 } 1729 } 1730 1731 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1732 String path = entry.mPath; 1733 ContentValues values = new ContentValues(); 1734 int lastSlash = path.lastIndexOf('/'); 1735 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1736 Uri uri, membersUri; 1737 long rowId = entry.mRowId; 1738 1739 // make sure we have a name 1740 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1741 if (name == null) { 1742 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1743 if (name == null) { 1744 // extract name from file name 1745 int lastDot = path.lastIndexOf('.'); 1746 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1747 : path.substring(lastSlash + 1, lastDot)); 1748 } 1749 } 1750 1751 values.put(MediaStore.Audio.Playlists.NAME, name); 1752 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1753 1754 if (rowId == 0) { 1755 values.put(MediaStore.Audio.Playlists.DATA, path); 1756 uri = mMediaProvider.insert(mPlaylistsUri, values); 1757 rowId = ContentUris.parseId(uri); 1758 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1759 } else { 1760 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1761 mMediaProvider.update(uri, values, null, null); 1762 1763 // delete members of existing playlist 1764 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1765 mMediaProvider.delete(membersUri, null, null); 1766 } 1767 1768 String playListDirectory = path.substring(0, lastSlash + 1); 1769 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1770 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1771 1772 if (fileType == MediaFile.FILE_TYPE_M3U) { 1773 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1774 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1775 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1776 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1777 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1778 } 1779 } 1780 1781 private void processPlayLists() throws RemoteException { 1782 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1783 Cursor fileList = null; 1784 try { 1785 // use the files uri and projection because we need the format column, 1786 // but restrict the query to just audio files 1787 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1788 "media_type=2", null, null, null); 1789 while (iterator.hasNext()) { 1790 FileEntry entry = iterator.next(); 1791 // only process playlist files if they are new or have been modified since the last scan 1792 if (entry.mLastModifiedChanged) { 1793 processPlayList(entry, fileList); 1794 } 1795 } 1796 } catch (RemoteException e1) { 1797 } finally { 1798 if (fileList != null) { 1799 fileList.close(); 1800 } 1801 } 1802 } 1803 1804 private native void processDirectory(String path, MediaScannerClient client); 1805 private native void processFile(String path, String mimeType, MediaScannerClient client); 1806 public native void setLocale(String locale); 1807 1808 public native byte[] extractAlbumArt(FileDescriptor fd); 1809 1810 private static native final void native_init(); 1811 private native final void native_setup(); 1812 private native final void native_finalize(); 1813 1814 /** 1815 * Releases resources associated with this MediaScanner object. 1816 * It is considered good practice to call this method when 1817 * one is done using the MediaScanner object. After this method 1818 * is called, the MediaScanner object can no longer be used. 1819 */ 1820 public void release() { 1821 native_finalize(); 1822 } 1823 1824 @Override 1825 protected void finalize() { 1826 mContext.getContentResolver().releaseProvider(mMediaProvider); 1827 native_finalize(); 1828 } 1829} 1830