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