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.system.ErrnoException; 48 import android.system.Os; 49 import android.text.TextUtils; 50 import android.util.Log; 51 import android.util.Xml; 52 53 import java.io.BufferedReader; 54 import java.io.File; 55 import java.io.FileDescriptor; 56 import java.io.FileInputStream; 57 import java.io.IOException; 58 import java.io.InputStreamReader; 59 import java.util.ArrayList; 60 import java.util.HashMap; 61 import java.util.HashSet; 62 import java.util.Iterator; 63 import java.util.Locale; 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"); native_init()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 long 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 FileEntry(long rowId, String path, long lastModified, int format)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 toString()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 MediaScanner(Context c)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 setDefaultRingtoneFileNames()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 isDrmEnabled()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 beginFile(String path, String mimeType, long lastModified, long fileSize, boolean isDirectory, boolean noMedia)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 path = Environment.maybeTranslateEmulatedPathToInternal(new File(path)) 557 .getAbsolutePath(); 558 } 559 560 // we only extract metadata for audio and video files 561 if (isaudio || isvideo) { 562 processFile(path, mimeType, this); 563 } 564 565 if (isimage) { 566 processImageFile(path); 567 } 568 569 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 570 } 571 } 572 } catch (RemoteException e) { 573 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 574 } 575 // long t2 = System.currentTimeMillis(); 576 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 577 return result; 578 } 579 580 private int parseSubstring(String s, int start, int defaultValue) { 581 int length = s.length(); 582 if (start == length) return defaultValue; 583 584 char ch = s.charAt(start++); 585 // return defaultValue if we have no integer at all 586 if (ch < '0' || ch > '9') return defaultValue; 587 588 int result = ch - '0'; 589 while (start < length) { 590 ch = s.charAt(start++); 591 if (ch < '0' || ch > '9') return result; 592 result = result * 10 + (ch - '0'); 593 } 594 595 return result; 596 } 597 598 public void handleStringTag(String name, String value) { 599 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 600 // Don't trim() here, to preserve the special \001 character 601 // used to force sorting. The media provider will trim() before 602 // inserting the title in to the database. 603 mTitle = value; 604 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 605 mArtist = value.trim(); 606 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") 607 || name.equalsIgnoreCase("band") || name.startsWith("band;")) { 608 mAlbumArtist = value.trim(); 609 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 610 mAlbum = value.trim(); 611 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 612 mComposer = value.trim(); 613 } else if (mProcessGenres && 614 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { 615 mGenre = getGenreName(value); 616 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 617 mYear = parseSubstring(value, 0, 0); 618 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 619 // track number might be of the form "2/12" 620 // we just read the number before the slash 621 int num = parseSubstring(value, 0, 0); 622 mTrack = (mTrack / 1000) * 1000 + num; 623 } else if (name.equalsIgnoreCase("discnumber") || 624 name.equals("set") || name.startsWith("set;")) { 625 // set number might be of the form "1/3" 626 // we just read the number before the slash 627 int num = parseSubstring(value, 0, 0); 628 mTrack = (num * 1000) + (mTrack % 1000); 629 } else if (name.equalsIgnoreCase("duration")) { 630 mDuration = parseSubstring(value, 0, 0); 631 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) { 632 mWriter = value.trim(); 633 } else if (name.equalsIgnoreCase("compilation")) { 634 mCompilation = parseSubstring(value, 0, 0); 635 } else if (name.equalsIgnoreCase("isdrm")) { 636 mIsDrm = (parseSubstring(value, 0, 0) == 1); 637 } else if (name.equalsIgnoreCase("width")) { 638 mWidth = parseSubstring(value, 0, 0); 639 } else if (name.equalsIgnoreCase("height")) { 640 mHeight = parseSubstring(value, 0, 0); 641 } else { 642 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")"); 643 } 644 } 645 646 private boolean convertGenreCode(String input, String expected) { 647 String output = getGenreName(input); 648 if (output.equals(expected)) { 649 return true; 650 } else { 651 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'"); 652 return false; 653 } 654 } 655 private void testGenreNameConverter() { 656 convertGenreCode("2", "Country"); 657 convertGenreCode("(2)", "Country"); 658 convertGenreCode("(2", "(2"); 659 convertGenreCode("2 Foo", "Country"); 660 convertGenreCode("(2) Foo", "Country"); 661 convertGenreCode("(2 Foo", "(2 Foo"); 662 convertGenreCode("2Foo", "2Foo"); 663 convertGenreCode("(2)Foo", "Country"); 664 convertGenreCode("200 Foo", "Foo"); 665 convertGenreCode("(200) Foo", "Foo"); 666 convertGenreCode("200Foo", "200Foo"); 667 convertGenreCode("(200)Foo", "Foo"); 668 convertGenreCode("200)Foo", "200)Foo"); 669 convertGenreCode("200) Foo", "200) Foo"); 670 } 671 672 public String getGenreName(String genreTagValue) { 673 674 if (genreTagValue == null) { 675 return null; 676 } 677 final int length = genreTagValue.length(); 678 679 if (length > 0) { 680 boolean parenthesized = false; 681 StringBuffer number = new StringBuffer(); 682 int i = 0; 683 for (; i < length; ++i) { 684 char c = genreTagValue.charAt(i); 685 if (i == 0 && c == '(') { 686 parenthesized = true; 687 } else if (Character.isDigit(c)) { 688 number.append(c); 689 } else { 690 break; 691 } 692 } 693 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' '; 694 if ((parenthesized && charAfterNumber == ')') 695 || !parenthesized && Character.isWhitespace(charAfterNumber)) { 696 try { 697 short genreIndex = Short.parseShort(number.toString()); 698 if (genreIndex >= 0) { 699 if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) { 700 return ID3_GENRES[genreIndex]; 701 } else if (genreIndex == 0xFF) { 702 return null; 703 } else if (genreIndex < 0xFF && (i + 1) < length) { 704 // genre is valid but unknown, 705 // if there is a string after the value we take it 706 if (parenthesized && charAfterNumber == ')') { 707 i++; 708 } 709 String ret = genreTagValue.substring(i).trim(); 710 if (ret.length() != 0) { 711 return ret; 712 } 713 } else { 714 // else return the number, without parentheses 715 return number.toString(); 716 } 717 } 718 } catch (NumberFormatException e) { 719 } 720 } 721 } 722 723 return genreTagValue; 724 } 725 726 private void processImageFile(String path) { 727 try { 728 mBitmapOptions.outWidth = 0; 729 mBitmapOptions.outHeight = 0; 730 BitmapFactory.decodeFile(path, mBitmapOptions); 731 mWidth = mBitmapOptions.outWidth; 732 mHeight = mBitmapOptions.outHeight; 733 } catch (Throwable th) { 734 // ignore; 735 } 736 } 737 738 public void setMimeType(String mimeType) { 739 if ("audio/mp4".equals(mMimeType) && 740 mimeType.startsWith("video")) { 741 // for feature parity with Donut, we force m4a files to keep the 742 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 743 // with a video track 744 return; 745 } 746 mMimeType = mimeType; 747 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 748 } 749 750 /** 751 * Formats the data into a values array suitable for use with the Media 752 * Content Provider. 753 * 754 * @return a map of values 755 */ 756 private ContentValues toValues() { 757 ContentValues map = new ContentValues(); 758 759 map.put(MediaStore.MediaColumns.DATA, mPath); 760 map.put(MediaStore.MediaColumns.TITLE, mTitle); 761 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 762 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 763 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 764 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm); 765 766 String resolution = null; 767 if (mWidth > 0 && mHeight > 0) { 768 map.put(MediaStore.MediaColumns.WIDTH, mWidth); 769 map.put(MediaStore.MediaColumns.HEIGHT, mHeight); 770 resolution = mWidth + "x" + mHeight; 771 } 772 773 if (!mNoMedia) { 774 if (MediaFile.isVideoFileType(mFileType)) { 775 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 776 ? mArtist : MediaStore.UNKNOWN_STRING)); 777 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 778 ? mAlbum : MediaStore.UNKNOWN_STRING)); 779 map.put(Video.Media.DURATION, mDuration); 780 if (resolution != null) { 781 map.put(Video.Media.RESOLUTION, resolution); 782 } 783 } else if (MediaFile.isImageFileType(mFileType)) { 784 // FIXME - add DESCRIPTION 785 } else if (MediaFile.isAudioFileType(mFileType)) { 786 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 787 mArtist : MediaStore.UNKNOWN_STRING); 788 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 789 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 790 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 791 mAlbum : MediaStore.UNKNOWN_STRING); 792 map.put(Audio.Media.COMPOSER, mComposer); 793 map.put(Audio.Media.GENRE, mGenre); 794 if (mYear != 0) { 795 map.put(Audio.Media.YEAR, mYear); 796 } 797 map.put(Audio.Media.TRACK, mTrack); 798 map.put(Audio.Media.DURATION, mDuration); 799 map.put(Audio.Media.COMPILATION, mCompilation); 800 } 801 } 802 return map; 803 } 804 805 private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications, 806 boolean alarms, boolean music, boolean podcasts) 807 throws RemoteException { 808 // update database 809 810 // use album artist if artist is missing 811 if (mArtist == null || mArtist.length() == 0) { 812 mArtist = mAlbumArtist; 813 } 814 815 ContentValues values = toValues(); 816 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 817 if (title == null || TextUtils.isEmpty(title.trim())) { 818 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 819 values.put(MediaStore.MediaColumns.TITLE, title); 820 } 821 String album = values.getAsString(Audio.Media.ALBUM); 822 if (MediaStore.UNKNOWN_STRING.equals(album)) { 823 album = values.getAsString(MediaStore.MediaColumns.DATA); 824 // extract last path segment before file name 825 int lastSlash = album.lastIndexOf('/'); 826 if (lastSlash >= 0) { 827 int previousSlash = 0; 828 while (true) { 829 int idx = album.indexOf('/', previousSlash + 1); 830 if (idx < 0 || idx >= lastSlash) { 831 break; 832 } 833 previousSlash = idx; 834 } 835 if (previousSlash != 0) { 836 album = album.substring(previousSlash + 1, lastSlash); 837 values.put(Audio.Media.ALBUM, album); 838 } 839 } 840 } 841 long rowId = entry.mRowId; 842 if (MediaFile.isAudioFileType(mFileType) && (rowId == 0 || mMtpObjectHandle != 0)) { 843 // Only set these for new entries. For existing entries, they 844 // may have been modified later, and we want to keep the current 845 // values so that custom ringtones still show up in the ringtone 846 // picker. 847 values.put(Audio.Media.IS_RINGTONE, ringtones); 848 values.put(Audio.Media.IS_NOTIFICATION, notifications); 849 values.put(Audio.Media.IS_ALARM, alarms); 850 values.put(Audio.Media.IS_MUSIC, music); 851 values.put(Audio.Media.IS_PODCAST, podcasts); 852 } else if (mFileType == MediaFile.FILE_TYPE_JPEG && !mNoMedia) { 853 ExifInterface exif = null; 854 try { 855 exif = new ExifInterface(entry.mPath); 856 } catch (IOException ex) { 857 // exif is null 858 } 859 if (exif != null) { 860 float[] latlng = new float[2]; 861 if (exif.getLatLong(latlng)) { 862 values.put(Images.Media.LATITUDE, latlng[0]); 863 values.put(Images.Media.LONGITUDE, latlng[1]); 864 } 865 866 long time = exif.getGpsDateTime(); 867 if (time != -1) { 868 values.put(Images.Media.DATE_TAKEN, time); 869 } else { 870 // If no time zone information is available, we should consider using 871 // EXIF local time as taken time if the difference between file time 872 // and EXIF local time is not less than 1 Day, otherwise MediaProvider 873 // will use file time as taken time. 874 time = exif.getDateTime(); 875 if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) { 876 values.put(Images.Media.DATE_TAKEN, time); 877 } 878 } 879 880 int orientation = exif.getAttributeInt( 881 ExifInterface.TAG_ORIENTATION, -1); 882 if (orientation != -1) { 883 // We only recognize a subset of orientation tag values. 884 int degree; 885 switch(orientation) { 886 case ExifInterface.ORIENTATION_ROTATE_90: 887 degree = 90; 888 break; 889 case ExifInterface.ORIENTATION_ROTATE_180: 890 degree = 180; 891 break; 892 case ExifInterface.ORIENTATION_ROTATE_270: 893 degree = 270; 894 break; 895 default: 896 degree = 0; 897 break; 898 } 899 values.put(Images.Media.ORIENTATION, degree); 900 } 901 } 902 } 903 904 Uri tableUri = mFilesUri; 905 MediaInserter inserter = mMediaInserter; 906 if (!mNoMedia) { 907 if (MediaFile.isVideoFileType(mFileType)) { 908 tableUri = mVideoUri; 909 } else if (MediaFile.isImageFileType(mFileType)) { 910 tableUri = mImagesUri; 911 } else if (MediaFile.isAudioFileType(mFileType)) { 912 tableUri = mAudioUri; 913 } 914 } 915 Uri result = null; 916 boolean needToSetSettings = false; 917 if (rowId == 0) { 918 if (mMtpObjectHandle != 0) { 919 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle); 920 } 921 if (tableUri == mFilesUri) { 922 int format = entry.mFormat; 923 if (format == 0) { 924 format = MediaFile.getFormatCode(entry.mPath, mMimeType); 925 } 926 values.put(Files.FileColumns.FORMAT, format); 927 } 928 // Setting a flag in order not to use bulk insert for the file related with 929 // notifications, ringtones, and alarms, because the rowId of the inserted file is 930 // needed. 931 if (mWasEmptyPriorToScan) { 932 if (notifications && !mDefaultNotificationSet) { 933 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 934 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 935 needToSetSettings = true; 936 } 937 } else if (ringtones && !mDefaultRingtoneSet) { 938 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 939 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 940 needToSetSettings = true; 941 } 942 } else if (alarms && !mDefaultAlarmSet) { 943 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 944 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 945 needToSetSettings = true; 946 } 947 } 948 } 949 950 // New file, insert it. 951 // Directories need to be inserted before the files they contain, so they 952 // get priority when bulk inserting. 953 // If the rowId of the inserted file is needed, it gets inserted immediately, 954 // bypassing the bulk inserter. 955 if (inserter == null || needToSetSettings) { 956 if (inserter != null) { 957 inserter.flushAll(); 958 } 959 result = mMediaProvider.insert(mPackageName, tableUri, values); 960 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 961 inserter.insertwithPriority(tableUri, values); 962 } else { 963 inserter.insert(tableUri, values); 964 } 965 966 if (result != null) { 967 rowId = ContentUris.parseId(result); 968 entry.mRowId = rowId; 969 } 970 } else { 971 // updated file 972 result = ContentUris.withAppendedId(tableUri, rowId); 973 // path should never change, and we want to avoid replacing mixed cased paths 974 // with squashed lower case paths 975 values.remove(MediaStore.MediaColumns.DATA); 976 977 int mediaType = 0; 978 if (!MediaScanner.isNoMediaPath(entry.mPath)) { 979 int fileType = MediaFile.getFileTypeForMimeType(mMimeType); 980 if (MediaFile.isAudioFileType(fileType)) { 981 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 982 } else if (MediaFile.isVideoFileType(fileType)) { 983 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 984 } else if (MediaFile.isImageFileType(fileType)) { 985 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 986 } else if (MediaFile.isPlayListFileType(fileType)) { 987 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 988 } 989 values.put(FileColumns.MEDIA_TYPE, mediaType); 990 } 991 mMediaProvider.update(mPackageName, result, values, null, null); 992 } 993 994 if(needToSetSettings) { 995 if (notifications) { 996 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 997 mDefaultNotificationSet = true; 998 } else if (ringtones) { 999 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 1000 mDefaultRingtoneSet = true; 1001 } else if (alarms) { 1002 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 1003 mDefaultAlarmSet = true; 1004 } 1005 } 1006 1007 return result; 1008 } 1009 1010 private boolean doesPathHaveFilename(String path, String filename) { 1011 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 1012 int filenameLength = filename.length(); 1013 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 1014 pathFilenameStart + filenameLength == path.length(); 1015 } 1016 1017 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 1018 1019 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 1020 settingName); 1021 1022 if (TextUtils.isEmpty(existingSettingValue)) { 1023 // Set the setting to the given URI 1024 Settings.System.putString(mContext.getContentResolver(), settingName, 1025 ContentUris.withAppendedId(uri, rowId).toString()); 1026 } 1027 } 1028 1029 private int getFileTypeFromDrm(String path) { 1030 if (!isDrmEnabled()) { 1031 return 0; 1032 } 1033 1034 int resultFileType = 0; 1035 1036 if (mDrmManagerClient == null) { 1037 mDrmManagerClient = new DrmManagerClient(mContext); 1038 } 1039 1040 if (mDrmManagerClient.canHandle(path, null)) { 1041 mIsDrm = true; 1042 String drmMimetype = mDrmManagerClient.getOriginalMimeType(path); 1043 if (drmMimetype != null) { 1044 mMimeType = drmMimetype; 1045 resultFileType = MediaFile.getFileTypeForMimeType(drmMimetype); 1046 } 1047 } 1048 return resultFileType; 1049 } 1050 1051 }; // end of anonymous MediaScannerClient instance 1052 1053 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 1054 Cursor c = null; 1055 String where = null; 1056 String[] selectionArgs = null; 1057 1058 if (mPlayLists == null) { 1059 mPlayLists = new ArrayList<FileEntry>(); 1060 } else { 1061 mPlayLists.clear(); 1062 } 1063 1064 if (filePath != null) { 1065 // query for only one file 1066 where = MediaStore.Files.FileColumns._ID + ">?" + 1067 " AND " + Files.FileColumns.DATA + "=?"; 1068 selectionArgs = new String[] { "", filePath }; 1069 } else { 1070 where = MediaStore.Files.FileColumns._ID + ">?"; 1071 selectionArgs = new String[] { "" }; 1072 } 1073 1074 // Tell the provider to not delete the file. 1075 // If the file is truly gone the delete is unnecessary, and we want to avoid 1076 // accidentally deleting files that are really there (this may happen if the 1077 // filesystem is mounted and unmounted while the scanner is running). 1078 Uri.Builder builder = mFilesUri.buildUpon(); 1079 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false"); 1080 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, mPackageName, 1081 builder.build()); 1082 1083 // Build the list of files from the content provider 1084 try { 1085 if (prescanFiles) { 1086 // First read existing files from the files table. 1087 // Because we'll be deleting entries for missing files as we go, 1088 // we need to query the database in small batches, to avoid problems 1089 // with CursorWindow positioning. 1090 long lastId = Long.MIN_VALUE; 1091 Uri limitUri = mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build(); 1092 mWasEmptyPriorToScan = true; 1093 1094 while (true) { 1095 selectionArgs[0] = "" + lastId; 1096 if (c != null) { 1097 c.close(); 1098 c = null; 1099 } 1100 c = mMediaProvider.query(mPackageName, limitUri, FILES_PRESCAN_PROJECTION, 1101 where, selectionArgs, MediaStore.Files.FileColumns._ID, null); 1102 if (c == null) { 1103 break; 1104 } 1105 1106 int num = c.getCount(); 1107 1108 if (num == 0) { 1109 break; 1110 } 1111 mWasEmptyPriorToScan = false; 1112 while (c.moveToNext()) { 1113 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1114 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1115 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1116 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1117 lastId = rowId; 1118 1119 // Only consider entries with absolute path names. 1120 // This allows storing URIs in the database without the 1121 // media scanner removing them. 1122 if (path != null && path.startsWith("/")) { 1123 boolean exists = false; 1124 try { 1125 exists = Os.access(path, android.system.OsConstants.F_OK); 1126 } catch (ErrnoException e1) { 1127 } 1128 if (!exists && !MtpConstants.isAbstractObject(format)) { 1129 // do not delete missing playlists, since they may have been 1130 // modified by the user. 1131 // The user can delete them in the media player instead. 1132 // instead, clear the path and lastModified fields in the row 1133 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1134 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1135 1136 if (!MediaFile.isPlayListFileType(fileType)) { 1137 deleter.delete(rowId); 1138 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 1139 deleter.flush(); 1140 String parent = new File(path).getParent(); 1141 mMediaProvider.call(mPackageName, MediaStore.UNHIDE_CALL, 1142 parent, null); 1143 } 1144 } 1145 } 1146 } 1147 } 1148 } 1149 } 1150 } 1151 finally { 1152 if (c != null) { 1153 c.close(); 1154 } 1155 deleter.flush(); 1156 } 1157 1158 // compute original size of images 1159 mOriginalCount = 0; 1160 c = mMediaProvider.query(mPackageName, mImagesUri, ID_PROJECTION, null, null, null, null); 1161 if (c != null) { 1162 mOriginalCount = c.getCount(); 1163 c.close(); 1164 } 1165 } 1166 1167 private boolean inScanDirectory(String path, String[] directories) { 1168 for (int i = 0; i < directories.length; i++) { 1169 String directory = directories[i]; 1170 if (path.startsWith(directory)) { 1171 return true; 1172 } 1173 } 1174 return false; 1175 } 1176 1177 private void pruneDeadThumbnailFiles() { 1178 HashSet<String> existingFiles = new HashSet<String>(); 1179 String directory = "/sdcard/DCIM/.thumbnails"; 1180 String [] files = (new File(directory)).list(); 1181 Cursor c = null; 1182 if (files == null) 1183 files = new String[0]; 1184 1185 for (int i = 0; i < files.length; i++) { 1186 String fullPathString = directory + "/" + files[i]; 1187 existingFiles.add(fullPathString); 1188 } 1189 1190 try { 1191 c = mMediaProvider.query( 1192 mPackageName, 1193 mThumbsUri, 1194 new String [] { "_data" }, 1195 null, 1196 null, 1197 null, null); 1198 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1199 if (c != null && c.moveToFirst()) { 1200 do { 1201 String fullPathString = c.getString(0); 1202 existingFiles.remove(fullPathString); 1203 } while (c.moveToNext()); 1204 } 1205 1206 for (String fileToDelete : existingFiles) { 1207 if (false) 1208 Log.v(TAG, "fileToDelete is " + fileToDelete); 1209 try { 1210 (new File(fileToDelete)).delete(); 1211 } catch (SecurityException ex) { 1212 } 1213 } 1214 1215 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1216 } catch (RemoteException e) { 1217 // We will soon be killed... 1218 } finally { 1219 if (c != null) { 1220 c.close(); 1221 } 1222 } 1223 } 1224 1225 static class MediaBulkDeleter { 1226 StringBuilder whereClause = new StringBuilder(); 1227 ArrayList<String> whereArgs = new ArrayList<String>(100); 1228 final IContentProvider mProvider; 1229 final String mPackageName; 1230 final Uri mBaseUri; 1231 1232 public MediaBulkDeleter(IContentProvider provider, String packageName, Uri baseUri) { 1233 mProvider = provider; 1234 mPackageName = packageName; 1235 mBaseUri = baseUri; 1236 } 1237 1238 public void delete(long id) throws RemoteException { 1239 if (whereClause.length() != 0) { 1240 whereClause.append(","); 1241 } 1242 whereClause.append("?"); 1243 whereArgs.add("" + id); 1244 if (whereArgs.size() > 100) { 1245 flush(); 1246 } 1247 } 1248 public void flush() throws RemoteException { 1249 int size = whereArgs.size(); 1250 if (size > 0) { 1251 String [] foo = new String [size]; 1252 foo = whereArgs.toArray(foo); 1253 int numrows = mProvider.delete(mPackageName, mBaseUri, 1254 MediaStore.MediaColumns._ID + " IN (" + 1255 whereClause.toString() + ")", foo); 1256 //Log.i("@@@@@@@@@", "rows deleted: " + numrows); 1257 whereClause.setLength(0); 1258 whereArgs.clear(); 1259 } 1260 } 1261 } 1262 1263 private void postscan(String[] directories) throws RemoteException { 1264 1265 // handle playlists last, after we know what media files are on the storage. 1266 if (mProcessPlaylists) { 1267 processPlayLists(); 1268 } 1269 1270 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1271 pruneDeadThumbnailFiles(); 1272 1273 // allow GC to clean up 1274 mPlayLists = null; 1275 mMediaProvider = null; 1276 } 1277 1278 private void releaseResources() { 1279 // release the DrmManagerClient resources 1280 if (mDrmManagerClient != null) { 1281 mDrmManagerClient.release(); 1282 mDrmManagerClient = null; 1283 } 1284 } 1285 1286 private void initialize(String volumeName) { 1287 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1288 1289 mAudioUri = Audio.Media.getContentUri(volumeName); 1290 mVideoUri = Video.Media.getContentUri(volumeName); 1291 mImagesUri = Images.Media.getContentUri(volumeName); 1292 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1293 mFilesUri = Files.getContentUri(volumeName); 1294 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); 1295 1296 if (!volumeName.equals("internal")) { 1297 // we only support playlists on external media 1298 mProcessPlaylists = true; 1299 mProcessGenres = true; 1300 mPlaylistsUri = Playlists.getContentUri(volumeName); 1301 1302 mCaseInsensitivePaths = true; 1303 } 1304 } 1305 1306 public void scanDirectories(String[] directories, String volumeName) { 1307 try { 1308 long start = System.currentTimeMillis(); 1309 initialize(volumeName); 1310 prescan(null, true); 1311 long prescan = System.currentTimeMillis(); 1312 1313 if (ENABLE_BULK_INSERTS) { 1314 // create MediaInserter for bulk inserts 1315 mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500); 1316 } 1317 1318 for (int i = 0; i < directories.length; i++) { 1319 processDirectory(directories[i], mClient); 1320 } 1321 1322 if (ENABLE_BULK_INSERTS) { 1323 // flush remaining inserts 1324 mMediaInserter.flushAll(); 1325 mMediaInserter = null; 1326 } 1327 1328 long scan = System.currentTimeMillis(); 1329 postscan(directories); 1330 long end = System.currentTimeMillis(); 1331 1332 if (false) { 1333 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1334 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1335 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1336 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1337 } 1338 } catch (SQLException e) { 1339 // this might happen if the SD card is removed while the media scanner is running 1340 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1341 } catch (UnsupportedOperationException e) { 1342 // this might happen if the SD card is removed while the media scanner is running 1343 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1344 } catch (RemoteException e) { 1345 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1346 } finally { 1347 releaseResources(); 1348 } 1349 } 1350 1351 // this function is used to scan a single file 1352 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1353 try { 1354 initialize(volumeName); 1355 prescan(path, true); 1356 1357 File file = new File(path); 1358 if (!file.exists()) { 1359 return null; 1360 } 1361 1362 // lastModified is in milliseconds on Files. 1363 long lastModifiedSeconds = file.lastModified() / 1000; 1364 1365 // always scan the file, so we can return the content://media Uri for existing files 1366 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1367 false, true, MediaScanner.isNoMediaPath(path)); 1368 } catch (RemoteException e) { 1369 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1370 return null; 1371 } finally { 1372 releaseResources(); 1373 } 1374 } 1375 1376 private static boolean isNoMediaFile(String path) { 1377 File file = new File(path); 1378 if (file.isDirectory()) return false; 1379 1380 // special case certain file names 1381 // I use regionMatches() instead of substring() below 1382 // to avoid memory allocation 1383 int lastSlash = path.lastIndexOf('/'); 1384 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1385 // ignore those ._* files created by MacOS 1386 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1387 return true; 1388 } 1389 1390 // ignore album art files created by Windows Media Player: 1391 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1392 // and AlbumArt_{...}_Small.jpg 1393 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1394 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1395 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1396 return true; 1397 } 1398 int length = path.length() - lastSlash - 1; 1399 if ((length == 17 && path.regionMatches( 1400 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1401 (length == 10 1402 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1403 return true; 1404 } 1405 } 1406 } 1407 return false; 1408 } 1409 1410 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>(); 1411 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>(); 1412 1413 /* MediaProvider calls this when a .nomedia file is added or removed */ 1414 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) { 1415 synchronized (MediaScanner.class) { 1416 if (clearMediaPaths) { 1417 mMediaPaths.clear(); 1418 } 1419 if (clearNoMediaPaths) { 1420 mNoMediaPaths.clear(); 1421 } 1422 } 1423 } 1424 1425 public static boolean isNoMediaPath(String path) { 1426 if (path == null) { 1427 return false; 1428 } 1429 // return true if file or any parent directory has name starting with a dot 1430 if (path.indexOf("/.") >= 0) { 1431 return true; 1432 } 1433 1434 int firstSlash = path.lastIndexOf('/'); 1435 if (firstSlash <= 0) { 1436 return false; 1437 } 1438 String parent = path.substring(0, firstSlash); 1439 1440 synchronized (MediaScanner.class) { 1441 if (mNoMediaPaths.containsKey(parent)) { 1442 return true; 1443 } else if (!mMediaPaths.containsKey(parent)) { 1444 // check to see if any parent directories have a ".nomedia" file 1445 // start from 1 so we don't bother checking in the root directory 1446 int offset = 1; 1447 while (offset >= 0) { 1448 int slashIndex = path.indexOf('/', offset); 1449 if (slashIndex > offset) { 1450 slashIndex++; // move past slash 1451 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1452 if (file.exists()) { 1453 // we have a .nomedia in one of the parent directories 1454 mNoMediaPaths.put(parent, ""); 1455 return true; 1456 } 1457 } 1458 offset = slashIndex; 1459 } 1460 mMediaPaths.put(parent, ""); 1461 } 1462 } 1463 1464 return isNoMediaFile(path); 1465 } 1466 1467 public void scanMtpFile(String path, String volumeName, int objectHandle, int format) { 1468 initialize(volumeName); 1469 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1470 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1471 File file = new File(path); 1472 long lastModifiedSeconds = file.lastModified() / 1000; 1473 1474 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1475 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) && 1476 !MediaFile.isDrmFileType(fileType)) { 1477 1478 // no need to use the media scanner, but we need to update last modified and file size 1479 ContentValues values = new ContentValues(); 1480 values.put(Files.FileColumns.SIZE, file.length()); 1481 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1482 try { 1483 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1484 mMediaProvider.update(mPackageName, Files.getMtpObjectsUri(volumeName), values, 1485 "_id=?", whereArgs); 1486 } catch (RemoteException e) { 1487 Log.e(TAG, "RemoteException in scanMtpFile", e); 1488 } 1489 return; 1490 } 1491 1492 mMtpObjectHandle = objectHandle; 1493 Cursor fileList = null; 1494 try { 1495 if (MediaFile.isPlayListFileType(fileType)) { 1496 // build file cache so we can look up tracks in the playlist 1497 prescan(null, true); 1498 1499 FileEntry entry = makeEntryFor(path); 1500 if (entry != null) { 1501 fileList = mMediaProvider.query(mPackageName, mFilesUri, 1502 FILES_PRESCAN_PROJECTION, null, null, null, null); 1503 processPlayList(entry, fileList); 1504 } 1505 } else { 1506 // MTP will create a file entry for us so we don't want to do it in prescan 1507 prescan(path, false); 1508 1509 // always scan the file, so we can return the content://media Uri for existing files 1510 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1511 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1512 } 1513 } catch (RemoteException e) { 1514 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1515 } finally { 1516 mMtpObjectHandle = 0; 1517 if (fileList != null) { 1518 fileList.close(); 1519 } 1520 releaseResources(); 1521 } 1522 } 1523 1524 FileEntry makeEntryFor(String path) { 1525 String where; 1526 String[] selectionArgs; 1527 1528 Cursor c = null; 1529 try { 1530 where = Files.FileColumns.DATA + "=?"; 1531 selectionArgs = new String[] { path }; 1532 c = mMediaProvider.query(mPackageName, mFilesUriNoNotify, FILES_PRESCAN_PROJECTION, 1533 where, selectionArgs, null, null); 1534 if (c.moveToFirst()) { 1535 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1536 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1537 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1538 return new FileEntry(rowId, path, lastModified, format); 1539 } 1540 } catch (RemoteException e) { 1541 } finally { 1542 if (c != null) { 1543 c.close(); 1544 } 1545 } 1546 return null; 1547 } 1548 1549 // returns the number of matching file/directory names, starting from the right 1550 private int matchPaths(String path1, String path2) { 1551 int result = 0; 1552 int end1 = path1.length(); 1553 int end2 = path2.length(); 1554 1555 while (end1 > 0 && end2 > 0) { 1556 int slash1 = path1.lastIndexOf('/', end1 - 1); 1557 int slash2 = path2.lastIndexOf('/', end2 - 1); 1558 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1559 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1560 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1561 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1562 if (start1 < 0) start1 = 0; else start1++; 1563 if (start2 < 0) start2 = 0; else start2++; 1564 int length = end1 - start1; 1565 if (end2 - start2 != length) break; 1566 if (path1.regionMatches(true, start1, path2, start2, length)) { 1567 result++; 1568 end1 = start1 - 1; 1569 end2 = start2 - 1; 1570 } else break; 1571 } 1572 1573 return result; 1574 } 1575 1576 private boolean matchEntries(long rowId, String data) { 1577 1578 int len = mPlaylistEntries.size(); 1579 boolean done = true; 1580 for (int i = 0; i < len; i++) { 1581 PlaylistEntry entry = mPlaylistEntries.get(i); 1582 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1583 continue; // this entry has been matched already 1584 } 1585 done = false; 1586 if (data.equalsIgnoreCase(entry.path)) { 1587 entry.bestmatchid = rowId; 1588 entry.bestmatchlevel = Integer.MAX_VALUE; 1589 continue; // no need for path matching 1590 } 1591 1592 int matchLength = matchPaths(data, entry.path); 1593 if (matchLength > entry.bestmatchlevel) { 1594 entry.bestmatchid = rowId; 1595 entry.bestmatchlevel = matchLength; 1596 } 1597 } 1598 return done; 1599 } 1600 1601 private void cachePlaylistEntry(String line, String playListDirectory) { 1602 PlaylistEntry entry = new PlaylistEntry(); 1603 // watch for trailing whitespace 1604 int entryLength = line.length(); 1605 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1606 // path should be longer than 3 characters. 1607 // avoid index out of bounds errors below by returning here. 1608 if (entryLength < 3) return; 1609 if (entryLength < line.length()) line = line.substring(0, entryLength); 1610 1611 // does entry appear to be an absolute path? 1612 // look for Unix or DOS absolute paths 1613 char ch1 = line.charAt(0); 1614 boolean fullPath = (ch1 == '/' || 1615 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1616 // if we have a relative path, combine entry with playListDirectory 1617 if (!fullPath) 1618 line = playListDirectory + line; 1619 entry.path = line; 1620 //FIXME - should we look for "../" within the path? 1621 1622 mPlaylistEntries.add(entry); 1623 } 1624 1625 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1626 fileList.moveToPosition(-1); 1627 while (fileList.moveToNext()) { 1628 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1629 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1630 if (matchEntries(rowId, data)) { 1631 break; 1632 } 1633 } 1634 1635 int len = mPlaylistEntries.size(); 1636 int index = 0; 1637 for (int i = 0; i < len; i++) { 1638 PlaylistEntry entry = mPlaylistEntries.get(i); 1639 if (entry.bestmatchlevel > 0) { 1640 try { 1641 values.clear(); 1642 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1643 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1644 mMediaProvider.insert(mPackageName, playlistUri, values); 1645 index++; 1646 } catch (RemoteException e) { 1647 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1648 return; 1649 } 1650 } 1651 } 1652 mPlaylistEntries.clear(); 1653 } 1654 1655 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1656 ContentValues values, Cursor fileList) { 1657 BufferedReader reader = null; 1658 try { 1659 File f = new File(path); 1660 if (f.exists()) { 1661 reader = new BufferedReader( 1662 new InputStreamReader(new FileInputStream(f)), 8192); 1663 String line = reader.readLine(); 1664 mPlaylistEntries.clear(); 1665 while (line != null) { 1666 // ignore comment lines, which begin with '#' 1667 if (line.length() > 0 && line.charAt(0) != '#') { 1668 cachePlaylistEntry(line, playListDirectory); 1669 } 1670 line = reader.readLine(); 1671 } 1672 1673 processCachedPlaylist(fileList, values, uri); 1674 } 1675 } catch (IOException e) { 1676 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1677 } finally { 1678 try { 1679 if (reader != null) 1680 reader.close(); 1681 } catch (IOException e) { 1682 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1683 } 1684 } 1685 } 1686 1687 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1688 ContentValues values, Cursor fileList) { 1689 BufferedReader reader = null; 1690 try { 1691 File f = new File(path); 1692 if (f.exists()) { 1693 reader = new BufferedReader( 1694 new InputStreamReader(new FileInputStream(f)), 8192); 1695 String line = reader.readLine(); 1696 mPlaylistEntries.clear(); 1697 while (line != null) { 1698 // ignore comment lines, which begin with '#' 1699 if (line.startsWith("File")) { 1700 int equals = line.indexOf('='); 1701 if (equals > 0) { 1702 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1703 } 1704 } 1705 line = reader.readLine(); 1706 } 1707 1708 processCachedPlaylist(fileList, values, uri); 1709 } 1710 } catch (IOException e) { 1711 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1712 } finally { 1713 try { 1714 if (reader != null) 1715 reader.close(); 1716 } catch (IOException e) { 1717 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1718 } 1719 } 1720 } 1721 1722 class WplHandler implements ElementListener { 1723 1724 final ContentHandler handler; 1725 String playListDirectory; 1726 1727 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1728 this.playListDirectory = playListDirectory; 1729 1730 RootElement root = new RootElement("smil"); 1731 Element body = root.getChild("body"); 1732 Element seq = body.getChild("seq"); 1733 Element media = seq.getChild("media"); 1734 media.setElementListener(this); 1735 1736 this.handler = root.getContentHandler(); 1737 } 1738 1739 @Override 1740 public void start(Attributes attributes) { 1741 String path = attributes.getValue("", "src"); 1742 if (path != null) { 1743 cachePlaylistEntry(path, playListDirectory); 1744 } 1745 } 1746 1747 @Override 1748 public void end() { 1749 } 1750 1751 ContentHandler getContentHandler() { 1752 return handler; 1753 } 1754 } 1755 1756 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1757 ContentValues values, Cursor fileList) { 1758 FileInputStream fis = null; 1759 try { 1760 File f = new File(path); 1761 if (f.exists()) { 1762 fis = new FileInputStream(f); 1763 1764 mPlaylistEntries.clear(); 1765 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1766 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1767 1768 processCachedPlaylist(fileList, values, uri); 1769 } 1770 } catch (SAXException e) { 1771 e.printStackTrace(); 1772 } catch (IOException e) { 1773 e.printStackTrace(); 1774 } finally { 1775 try { 1776 if (fis != null) 1777 fis.close(); 1778 } catch (IOException e) { 1779 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1780 } 1781 } 1782 } 1783 1784 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1785 String path = entry.mPath; 1786 ContentValues values = new ContentValues(); 1787 int lastSlash = path.lastIndexOf('/'); 1788 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1789 Uri uri, membersUri; 1790 long rowId = entry.mRowId; 1791 1792 // make sure we have a name 1793 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1794 if (name == null) { 1795 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1796 if (name == null) { 1797 // extract name from file name 1798 int lastDot = path.lastIndexOf('.'); 1799 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1800 : path.substring(lastSlash + 1, lastDot)); 1801 } 1802 } 1803 1804 values.put(MediaStore.Audio.Playlists.NAME, name); 1805 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1806 1807 if (rowId == 0) { 1808 values.put(MediaStore.Audio.Playlists.DATA, path); 1809 uri = mMediaProvider.insert(mPackageName, mPlaylistsUri, values); 1810 rowId = ContentUris.parseId(uri); 1811 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1812 } else { 1813 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1814 mMediaProvider.update(mPackageName, uri, values, null, null); 1815 1816 // delete members of existing playlist 1817 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1818 mMediaProvider.delete(mPackageName, membersUri, null, null); 1819 } 1820 1821 String playListDirectory = path.substring(0, lastSlash + 1); 1822 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1823 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1824 1825 if (fileType == MediaFile.FILE_TYPE_M3U) { 1826 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1827 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1828 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1829 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1830 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1831 } 1832 } 1833 1834 private void processPlayLists() throws RemoteException { 1835 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1836 Cursor fileList = null; 1837 try { 1838 // use the files uri and projection because we need the format column, 1839 // but restrict the query to just audio files 1840 fileList = mMediaProvider.query(mPackageName, mFilesUri, FILES_PRESCAN_PROJECTION, 1841 "media_type=2", null, null, null); 1842 while (iterator.hasNext()) { 1843 FileEntry entry = iterator.next(); 1844 // only process playlist files if they are new or have been modified since the last scan 1845 if (entry.mLastModifiedChanged) { 1846 processPlayList(entry, fileList); 1847 } 1848 } 1849 } catch (RemoteException e1) { 1850 } finally { 1851 if (fileList != null) { 1852 fileList.close(); 1853 } 1854 } 1855 } 1856 1857 private native void processDirectory(String path, MediaScannerClient client); 1858 private native void processFile(String path, String mimeType, MediaScannerClient client); 1859 public native void setLocale(String locale); 1860 1861 public native byte[] extractAlbumArt(FileDescriptor fd); 1862 1863 private static native final void native_init(); 1864 private native final void native_setup(); 1865 private native final void native_finalize(); 1866 1867 /** 1868 * Releases resources associated with this MediaScanner object. 1869 * It is considered good practice to call this method when 1870 * one is done using the MediaScanner object. After this method 1871 * is called, the MediaScanner object can no longer be used. 1872 */ 1873 public void release() { 1874 native_finalize(); 1875 } 1876 1877 @Override 1878 protected void finalize() { 1879 mContext.getContentResolver().releaseProvider(mMediaProvider); 1880 native_finalize(); 1881 } 1882 } 1883