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