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