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