MediaScanner.java revision 299942002b17f792e6e4f6b3f6972f62b8b136ae
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 if (inserter != null) { 962 inserter.flushAll(); 963 } 964 result = mMediaProvider.insert(tableUri, values); 965 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 966 inserter.insertwithPriority(tableUri, values); 967 } else { 968 inserter.insert(tableUri, values); 969 } 970 971 if (result != null) { 972 rowId = ContentUris.parseId(result); 973 entry.mRowId = rowId; 974 } 975 } else { 976 // updated file 977 result = ContentUris.withAppendedId(tableUri, rowId); 978 // path should never change, and we want to avoid replacing mixed cased paths 979 // with squashed lower case paths 980 values.remove(MediaStore.MediaColumns.DATA); 981 982 int mediaType = 0; 983 if (!MediaScanner.isNoMediaPath(entry.mPath)) { 984 int fileType = MediaFile.getFileTypeForMimeType(mMimeType); 985 if (MediaFile.isAudioFileType(fileType)) { 986 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 987 } else if (MediaFile.isVideoFileType(fileType)) { 988 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 989 } else if (MediaFile.isImageFileType(fileType)) { 990 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 991 } else if (MediaFile.isPlayListFileType(fileType)) { 992 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 993 } 994 values.put(FileColumns.MEDIA_TYPE, mediaType); 995 } 996 mMediaProvider.update(result, values, null, null); 997 } 998 999 if(needToSetSettings) { 1000 if (notifications) { 1001 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 1002 mDefaultNotificationSet = true; 1003 } else if (ringtones) { 1004 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 1005 mDefaultRingtoneSet = true; 1006 } else if (alarms) { 1007 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 1008 mDefaultAlarmSet = true; 1009 } 1010 } 1011 1012 return result; 1013 } 1014 1015 private boolean doesPathHaveFilename(String path, String filename) { 1016 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 1017 int filenameLength = filename.length(); 1018 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 1019 pathFilenameStart + filenameLength == path.length(); 1020 } 1021 1022 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 1023 1024 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 1025 settingName); 1026 1027 if (TextUtils.isEmpty(existingSettingValue)) { 1028 // Set the setting to the given URI 1029 Settings.System.putString(mContext.getContentResolver(), settingName, 1030 ContentUris.withAppendedId(uri, rowId).toString()); 1031 } 1032 } 1033 1034 private int getFileTypeFromDrm(String path) { 1035 if (!isDrmEnabled()) { 1036 return 0; 1037 } 1038 1039 int resultFileType = 0; 1040 1041 if (mDrmManagerClient == null) { 1042 mDrmManagerClient = new DrmManagerClient(mContext); 1043 } 1044 1045 if (mDrmManagerClient.canHandle(path, null)) { 1046 mIsDrm = true; 1047 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path); 1048 if (drmMimetype != null) { 1049 mMimeType = drmMimetype; 1050 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype); 1051 } 1052 } 1053 return resultFileType; 1054 } 1055 1056 }; // end of anonymous MediaScannerClient instance 1057 1058 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 1059 Cursor c = null; 1060 String where = null; 1061 String[] selectionArgs = null; 1062 1063 if (mPlayLists == null) { 1064 mPlayLists = new ArrayList<FileEntry>(); 1065 } else { 1066 mPlayLists.clear(); 1067 } 1068 1069 if (filePath != null) { 1070 // query for only one file 1071 where = MediaStore.Files.FileColumns._ID + ">?" + 1072 " AND " + Files.FileColumns.DATA + "=?"; 1073 selectionArgs = new String[] { "", filePath }; 1074 } else { 1075 where = MediaStore.Files.FileColumns._ID + ">?"; 1076 selectionArgs = new String[] { "" }; 1077 } 1078 1079 // Tell the provider to not delete the file. 1080 // If the file is truly gone the delete is unnecessary, and we want to avoid 1081 // accidentally deleting files that are really there (this may happen if the 1082 // filesystem is mounted and unmounted while the scanner is running). 1083 Uri.Builder builder = mFilesUri.buildUpon(); 1084 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false"); 1085 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build()); 1086 1087 // Build the list of files from the content provider 1088 try { 1089 if (prescanFiles) { 1090 // First read existing files from the files table. 1091 // Because we'll be deleting entries for missing files as we go, 1092 // we need to query the database in small batches, to avoid problems 1093 // with CursorWindow positioning. 1094 long lastId = Long.MIN_VALUE; 1095 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build(); 1096 mWasEmptyPriorToScan = true; 1097 1098 while (true) { 1099 selectionArgs[0] = "" + lastId; 1100 if (c != null) { 1101 c.close(); 1102 c = null; 1103 } 1104 c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION, 1105 where, selectionArgs, MediaStore.Files.FileColumns._ID, null); 1106 if (c == null) { 1107 break; 1108 } 1109 1110 int num = c.getCount(); 1111 1112 if (num == 0) { 1113 break; 1114 } 1115 mWasEmptyPriorToScan = false; 1116 while (c.moveToNext()) { 1117 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1118 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1119 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1120 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1121 lastId = rowId; 1122 1123 // Only consider entries with absolute path names. 1124 // This allows storing URIs in the database without the 1125 // media scanner removing them. 1126 if (path != null && path.startsWith("/")) { 1127 boolean exists = false; 1128 try { 1129 exists = Libcore.os.access(path, libcore.io.OsConstants.F_OK); 1130 } catch (ErrnoException e1) { 1131 } 1132 if (!exists && !MtpConstants.isAbstractObject(format)) { 1133 // do not delete missing playlists, since they may have been 1134 // modified by the user. 1135 // The user can delete them in the media player instead. 1136 // instead, clear the path and lastModified fields in the row 1137 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1138 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1139 1140 if (!MediaFile.isPlayListFileType(fileType)) { 1141 deleter.delete(rowId); 1142 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 1143 deleter.flush(); 1144 String parent = new File(path).getParent(); 1145 mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null); 1146 } 1147 } 1148 } 1149 } 1150 } 1151 } 1152 } 1153 } 1154 finally { 1155 if (c != null) { 1156 c.close(); 1157 } 1158 deleter.flush(); 1159 } 1160 1161 // compute original size of images 1162 mOriginalCount = 0; 1163 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null); 1164 if (c != null) { 1165 mOriginalCount = c.getCount(); 1166 c.close(); 1167 } 1168 } 1169 1170 private boolean inScanDirectory(String path, String[] directories) { 1171 for (int i = 0; i < directories.length; i++) { 1172 String directory = directories[i]; 1173 if (path.startsWith(directory)) { 1174 return true; 1175 } 1176 } 1177 return false; 1178 } 1179 1180 private void pruneDeadThumbnailFiles() { 1181 HashSet<String> existingFiles = new HashSet<String>(); 1182 String directory = "/sdcard/DCIM/.thumbnails"; 1183 String [] files = (new File(directory)).list(); 1184 if (files == null) 1185 files = new String[0]; 1186 1187 for (int i = 0; i < files.length; i++) { 1188 String fullPathString = directory + "/" + files[i]; 1189 existingFiles.add(fullPathString); 1190 } 1191 1192 try { 1193 Cursor c = mMediaProvider.query( 1194 mThumbsUri, 1195 new String [] { "_data" }, 1196 null, 1197 null, 1198 null, null); 1199 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1200 if (c != null && c.moveToFirst()) { 1201 do { 1202 String fullPathString = c.getString(0); 1203 existingFiles.remove(fullPathString); 1204 } while (c.moveToNext()); 1205 } 1206 1207 for (String fileToDelete : existingFiles) { 1208 if (false) 1209 Log.v(TAG, "fileToDelete is " + fileToDelete); 1210 try { 1211 (new File(fileToDelete)).delete(); 1212 } catch (SecurityException ex) { 1213 } 1214 } 1215 1216 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1217 if (c != null) { 1218 c.close(); 1219 } 1220 } catch (RemoteException e) { 1221 // We will soon be killed... 1222 } 1223 } 1224 1225 static class MediaBulkDeleter { 1226 StringBuilder whereClause = new StringBuilder(); 1227 ArrayList<String> whereArgs = new ArrayList<String>(100); 1228 IContentProvider mProvider; 1229 Uri mBaseUri; 1230 1231 public MediaBulkDeleter(IContentProvider provider, Uri baseUri) { 1232 mProvider = provider; 1233 mBaseUri = baseUri; 1234 } 1235 1236 public void delete(long id) throws RemoteException { 1237 if (whereClause.length() != 0) { 1238 whereClause.append(","); 1239 } 1240 whereClause.append("?"); 1241 whereArgs.add("" + id); 1242 if (whereArgs.size() > 100) { 1243 flush(); 1244 } 1245 } 1246 public void flush() throws RemoteException { 1247 int size = whereArgs.size(); 1248 if (size > 0) { 1249 String [] foo = new String [size]; 1250 foo = whereArgs.toArray(foo); 1251 int numrows = mProvider.delete(mBaseUri, MediaStore.MediaColumns._ID + " IN (" + 1252 whereClause.toString() + ")", foo); 1253 //Log.i("@@@@@@@@@", "rows deleted: " + numrows); 1254 whereClause.setLength(0); 1255 whereArgs.clear(); 1256 } 1257 } 1258 } 1259 1260 private void postscan(String[] directories) throws RemoteException { 1261 1262 // handle playlists last, after we know what media files are on the storage. 1263 if (mProcessPlaylists) { 1264 processPlayLists(); 1265 } 1266 1267 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1268 pruneDeadThumbnailFiles(); 1269 1270 // allow GC to clean up 1271 mPlayLists = null; 1272 mMediaProvider = null; 1273 } 1274 1275 private void initialize(String volumeName) { 1276 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1277 1278 mAudioUri = Audio.Media.getContentUri(volumeName); 1279 mVideoUri = Video.Media.getContentUri(volumeName); 1280 mImagesUri = Images.Media.getContentUri(volumeName); 1281 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1282 mFilesUri = Files.getContentUri(volumeName); 1283 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); 1284 1285 if (!volumeName.equals("internal")) { 1286 // we only support playlists on external media 1287 mProcessPlaylists = true; 1288 mProcessGenres = true; 1289 mPlaylistsUri = Playlists.getContentUri(volumeName); 1290 1291 mCaseInsensitivePaths = true; 1292 } 1293 } 1294 1295 public void scanDirectories(String[] directories, String volumeName) { 1296 try { 1297 long start = System.currentTimeMillis(); 1298 initialize(volumeName); 1299 prescan(null, true); 1300 long prescan = System.currentTimeMillis(); 1301 1302 if (ENABLE_BULK_INSERTS) { 1303 // create MediaInserter for bulk inserts 1304 mMediaInserter = new MediaInserter(mMediaProvider, 500); 1305 } 1306 1307 for (int i = 0; i < directories.length; i++) { 1308 processDirectory(directories[i], mClient); 1309 } 1310 1311 if (ENABLE_BULK_INSERTS) { 1312 // flush remaining inserts 1313 mMediaInserter.flushAll(); 1314 mMediaInserter = null; 1315 } 1316 1317 long scan = System.currentTimeMillis(); 1318 postscan(directories); 1319 long end = System.currentTimeMillis(); 1320 1321 if (false) { 1322 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1323 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1324 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1325 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1326 } 1327 } catch (SQLException e) { 1328 // this might happen if the SD card is removed while the media scanner is running 1329 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1330 } catch (UnsupportedOperationException e) { 1331 // this might happen if the SD card is removed while the media scanner is running 1332 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1333 } catch (RemoteException e) { 1334 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1335 } 1336 } 1337 1338 // this function is used to scan a single file 1339 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1340 try { 1341 initialize(volumeName); 1342 prescan(path, true); 1343 1344 File file = new File(path); 1345 if (!file.exists()) { 1346 return null; 1347 } 1348 1349 // lastModified is in milliseconds on Files. 1350 long lastModifiedSeconds = file.lastModified() / 1000; 1351 1352 // always scan the file, so we can return the content://media Uri for existing files 1353 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1354 false, true, false); 1355 } catch (RemoteException e) { 1356 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1357 return null; 1358 } 1359 } 1360 1361 private static boolean isNoMediaFile(String path) { 1362 File file = new File(path); 1363 if (file.isDirectory()) return false; 1364 1365 // special case certain file names 1366 // I use regionMatches() instead of substring() below 1367 // to avoid memory allocation 1368 int lastSlash = path.lastIndexOf('/'); 1369 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1370 // ignore those ._* files created by MacOS 1371 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1372 return true; 1373 } 1374 1375 // ignore album art files created by Windows Media Player: 1376 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1377 // and AlbumArt_{...}_Small.jpg 1378 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1379 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1380 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1381 return true; 1382 } 1383 int length = path.length() - lastSlash - 1; 1384 if ((length == 17 && path.regionMatches( 1385 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1386 (length == 10 1387 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1388 return true; 1389 } 1390 } 1391 } 1392 return false; 1393 } 1394 1395 public static boolean isNoMediaPath(String path) { 1396 if (path == null) return false; 1397 1398 // return true if file or any parent directory has name starting with a dot 1399 if (path.indexOf("/.") >= 0) return true; 1400 1401 // now check to see if any parent directories have a ".nomedia" file 1402 // start from 1 so we don't bother checking in the root directory 1403 int offset = 1; 1404 while (offset >= 0) { 1405 int slashIndex = path.indexOf('/', offset); 1406 if (slashIndex > offset) { 1407 slashIndex++; // move past slash 1408 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1409 if (file.exists()) { 1410 // we have a .nomedia in one of the parent directories 1411 return true; 1412 } 1413 } 1414 offset = slashIndex; 1415 } 1416 return isNoMediaFile(path); 1417 } 1418 1419 public void scanMtpFile(String path, String volumeName, int objectHandle, int format) { 1420 initialize(volumeName); 1421 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1422 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1423 File file = new File(path); 1424 long lastModifiedSeconds = file.lastModified() / 1000; 1425 1426 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1427 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) && 1428 !MediaFile.isDrmFileType(fileType)) { 1429 1430 // no need to use the media scanner, but we need to update last modified and file size 1431 ContentValues values = new ContentValues(); 1432 values.put(Files.FileColumns.SIZE, file.length()); 1433 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1434 try { 1435 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1436 mMediaProvider.update(Files.getMtpObjectsUri(volumeName), values, "_id=?", 1437 whereArgs); 1438 } catch (RemoteException e) { 1439 Log.e(TAG, "RemoteException in scanMtpFile", e); 1440 } 1441 return; 1442 } 1443 1444 mMtpObjectHandle = objectHandle; 1445 Cursor fileList = null; 1446 try { 1447 if (MediaFile.isPlayListFileType(fileType)) { 1448 // build file cache so we can look up tracks in the playlist 1449 prescan(null, true); 1450 1451 FileEntry entry = makeEntryFor(path); 1452 if (entry != null) { 1453 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1454 null, null, null, null); 1455 processPlayList(entry, fileList); 1456 } 1457 } else { 1458 // MTP will create a file entry for us so we don't want to do it in prescan 1459 prescan(path, false); 1460 1461 // always scan the file, so we can return the content://media Uri for existing files 1462 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1463 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1464 } 1465 } catch (RemoteException e) { 1466 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1467 } finally { 1468 mMtpObjectHandle = 0; 1469 if (fileList != null) { 1470 fileList.close(); 1471 } 1472 } 1473 } 1474 1475 FileEntry makeEntryFor(String path) { 1476 String where; 1477 String[] selectionArgs; 1478 1479 Cursor c = null; 1480 try { 1481 where = Files.FileColumns.DATA + "=?"; 1482 selectionArgs = new String[] { path }; 1483 c = mMediaProvider.query(mFilesUriNoNotify, FILES_PRESCAN_PROJECTION, 1484 where, selectionArgs, null, null); 1485 if (c.moveToFirst()) { 1486 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1487 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1488 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1489 return new FileEntry(rowId, path, lastModified, format); 1490 } 1491 } catch (RemoteException e) { 1492 } finally { 1493 if (c != null) { 1494 c.close(); 1495 } 1496 } 1497 return null; 1498 } 1499 1500 // returns the number of matching file/directory names, starting from the right 1501 private int matchPaths(String path1, String path2) { 1502 int result = 0; 1503 int end1 = path1.length(); 1504 int end2 = path2.length(); 1505 1506 while (end1 > 0 && end2 > 0) { 1507 int slash1 = path1.lastIndexOf('/', end1 - 1); 1508 int slash2 = path2.lastIndexOf('/', end2 - 1); 1509 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1510 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1511 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1512 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1513 if (start1 < 0) start1 = 0; else start1++; 1514 if (start2 < 0) start2 = 0; else start2++; 1515 int length = end1 - start1; 1516 if (end2 - start2 != length) break; 1517 if (path1.regionMatches(true, start1, path2, start2, length)) { 1518 result++; 1519 end1 = start1 - 1; 1520 end2 = start2 - 1; 1521 } else break; 1522 } 1523 1524 return result; 1525 } 1526 1527 private boolean matchEntries(long rowId, String data) { 1528 1529 int len = mPlaylistEntries.size(); 1530 boolean done = true; 1531 for (int i = 0; i < len; i++) { 1532 PlaylistEntry entry = mPlaylistEntries.get(i); 1533 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1534 continue; // this entry has been matched already 1535 } 1536 done = false; 1537 if (data.equalsIgnoreCase(entry.path)) { 1538 entry.bestmatchid = rowId; 1539 entry.bestmatchlevel = Integer.MAX_VALUE; 1540 continue; // no need for path matching 1541 } 1542 1543 int matchLength = matchPaths(data, entry.path); 1544 if (matchLength > entry.bestmatchlevel) { 1545 entry.bestmatchid = rowId; 1546 entry.bestmatchlevel = matchLength; 1547 } 1548 } 1549 return done; 1550 } 1551 1552 private void cachePlaylistEntry(String line, String playListDirectory) { 1553 PlaylistEntry entry = new PlaylistEntry(); 1554 // watch for trailing whitespace 1555 int entryLength = line.length(); 1556 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1557 // path should be longer than 3 characters. 1558 // avoid index out of bounds errors below by returning here. 1559 if (entryLength < 3) return; 1560 if (entryLength < line.length()) line = line.substring(0, entryLength); 1561 1562 // does entry appear to be an absolute path? 1563 // look for Unix or DOS absolute paths 1564 char ch1 = line.charAt(0); 1565 boolean fullPath = (ch1 == '/' || 1566 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1567 // if we have a relative path, combine entry with playListDirectory 1568 if (!fullPath) 1569 line = playListDirectory + line; 1570 entry.path = line; 1571 //FIXME - should we look for "../" within the path? 1572 1573 mPlaylistEntries.add(entry); 1574 } 1575 1576 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1577 fileList.moveToPosition(-1); 1578 while (fileList.moveToNext()) { 1579 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1580 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1581 if (matchEntries(rowId, data)) { 1582 break; 1583 } 1584 } 1585 1586 int len = mPlaylistEntries.size(); 1587 int index = 0; 1588 for (int i = 0; i < len; i++) { 1589 PlaylistEntry entry = mPlaylistEntries.get(i); 1590 if (entry.bestmatchlevel > 0) { 1591 try { 1592 values.clear(); 1593 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1594 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1595 mMediaProvider.insert(playlistUri, values); 1596 index++; 1597 } catch (RemoteException e) { 1598 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1599 return; 1600 } 1601 } 1602 } 1603 mPlaylistEntries.clear(); 1604 } 1605 1606 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1607 ContentValues values, Cursor fileList) { 1608 BufferedReader reader = null; 1609 try { 1610 File f = new File(path); 1611 if (f.exists()) { 1612 reader = new BufferedReader( 1613 new InputStreamReader(new FileInputStream(f)), 8192); 1614 String line = reader.readLine(); 1615 mPlaylistEntries.clear(); 1616 while (line != null) { 1617 // ignore comment lines, which begin with '#' 1618 if (line.length() > 0 && line.charAt(0) != '#') { 1619 cachePlaylistEntry(line, playListDirectory); 1620 } 1621 line = reader.readLine(); 1622 } 1623 1624 processCachedPlaylist(fileList, values, uri); 1625 } 1626 } catch (IOException e) { 1627 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1628 } finally { 1629 try { 1630 if (reader != null) 1631 reader.close(); 1632 } catch (IOException e) { 1633 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1634 } 1635 } 1636 } 1637 1638 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1639 ContentValues values, Cursor fileList) { 1640 BufferedReader reader = null; 1641 try { 1642 File f = new File(path); 1643 if (f.exists()) { 1644 reader = new BufferedReader( 1645 new InputStreamReader(new FileInputStream(f)), 8192); 1646 String line = reader.readLine(); 1647 mPlaylistEntries.clear(); 1648 while (line != null) { 1649 // ignore comment lines, which begin with '#' 1650 if (line.startsWith("File")) { 1651 int equals = line.indexOf('='); 1652 if (equals > 0) { 1653 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1654 } 1655 } 1656 line = reader.readLine(); 1657 } 1658 1659 processCachedPlaylist(fileList, values, uri); 1660 } 1661 } catch (IOException e) { 1662 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1663 } finally { 1664 try { 1665 if (reader != null) 1666 reader.close(); 1667 } catch (IOException e) { 1668 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1669 } 1670 } 1671 } 1672 1673 class WplHandler implements ElementListener { 1674 1675 final ContentHandler handler; 1676 String playListDirectory; 1677 1678 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1679 this.playListDirectory = playListDirectory; 1680 1681 RootElement root = new RootElement("smil"); 1682 Element body = root.getChild("body"); 1683 Element seq = body.getChild("seq"); 1684 Element media = seq.getChild("media"); 1685 media.setElementListener(this); 1686 1687 this.handler = root.getContentHandler(); 1688 } 1689 1690 @Override 1691 public void start(Attributes attributes) { 1692 String path = attributes.getValue("", "src"); 1693 if (path != null) { 1694 cachePlaylistEntry(path, playListDirectory); 1695 } 1696 } 1697 1698 @Override 1699 public void end() { 1700 } 1701 1702 ContentHandler getContentHandler() { 1703 return handler; 1704 } 1705 } 1706 1707 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1708 ContentValues values, Cursor fileList) { 1709 FileInputStream fis = null; 1710 try { 1711 File f = new File(path); 1712 if (f.exists()) { 1713 fis = new FileInputStream(f); 1714 1715 mPlaylistEntries.clear(); 1716 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1717 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1718 1719 processCachedPlaylist(fileList, values, uri); 1720 } 1721 } catch (SAXException e) { 1722 e.printStackTrace(); 1723 } catch (IOException e) { 1724 e.printStackTrace(); 1725 } finally { 1726 try { 1727 if (fis != null) 1728 fis.close(); 1729 } catch (IOException e) { 1730 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1731 } 1732 } 1733 } 1734 1735 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1736 String path = entry.mPath; 1737 ContentValues values = new ContentValues(); 1738 int lastSlash = path.lastIndexOf('/'); 1739 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1740 Uri uri, membersUri; 1741 long rowId = entry.mRowId; 1742 1743 // make sure we have a name 1744 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1745 if (name == null) { 1746 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1747 if (name == null) { 1748 // extract name from file name 1749 int lastDot = path.lastIndexOf('.'); 1750 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1751 : path.substring(lastSlash + 1, lastDot)); 1752 } 1753 } 1754 1755 values.put(MediaStore.Audio.Playlists.NAME, name); 1756 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1757 1758 if (rowId == 0) { 1759 values.put(MediaStore.Audio.Playlists.DATA, path); 1760 uri = mMediaProvider.insert(mPlaylistsUri, values); 1761 rowId = ContentUris.parseId(uri); 1762 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1763 } else { 1764 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1765 mMediaProvider.update(uri, values, null, null); 1766 1767 // delete members of existing playlist 1768 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1769 mMediaProvider.delete(membersUri, null, null); 1770 } 1771 1772 String playListDirectory = path.substring(0, lastSlash + 1); 1773 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1774 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1775 1776 if (fileType == MediaFile.FILE_TYPE_M3U) { 1777 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1778 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1779 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1780 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1781 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1782 } 1783 } 1784 1785 private void processPlayLists() throws RemoteException { 1786 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1787 Cursor fileList = null; 1788 try { 1789 // use the files uri and projection because we need the format column, 1790 // but restrict the query to just audio files 1791 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1792 "media_type=2", null, null, null); 1793 while (iterator.hasNext()) { 1794 FileEntry entry = iterator.next(); 1795 // only process playlist files if they are new or have been modified since the last scan 1796 if (entry.mLastModifiedChanged) { 1797 processPlayList(entry, fileList); 1798 } 1799 } 1800 } catch (RemoteException e1) { 1801 } finally { 1802 if (fileList != null) { 1803 fileList.close(); 1804 } 1805 } 1806 } 1807 1808 private native void processDirectory(String path, MediaScannerClient client); 1809 private native void processFile(String path, String mimeType, MediaScannerClient client); 1810 public native void setLocale(String locale); 1811 1812 public native byte[] extractAlbumArt(FileDescriptor fd); 1813 1814 private static native final void native_init(); 1815 private native final void native_setup(); 1816 private native final void native_finalize(); 1817 1818 /** 1819 * Releases resources associated with this MediaScanner object. 1820 * It is considered good practice to call this method when 1821 * one is done using the MediaScanner object. After this method 1822 * is called, the MediaScanner object can no longer be used. 1823 */ 1824 public void release() { 1825 native_finalize(); 1826 } 1827 1828 @Override 1829 protected void finalize() { 1830 mContext.getContentResolver().releaseProvider(mMediaProvider); 1831 native_finalize(); 1832 } 1833} 1834