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