1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.media; 18 19import org.xml.sax.Attributes; 20import org.xml.sax.ContentHandler; 21import org.xml.sax.SAXException; 22 23import android.content.ContentUris; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.IContentProvider; 27import android.database.Cursor; 28import android.database.SQLException; 29import android.drm.DrmManagerClient; 30import android.graphics.BitmapFactory; 31import android.mtp.MtpConstants; 32import android.net.Uri; 33import android.os.Environment; 34import android.os.RemoteException; 35import android.os.SystemProperties; 36import android.provider.MediaStore; 37import android.provider.MediaStore.Audio; 38import android.provider.MediaStore.Audio.Playlists; 39import android.provider.MediaStore.Files; 40import android.provider.MediaStore.Files.FileColumns; 41import android.provider.MediaStore.Images; 42import android.provider.MediaStore.Video; 43import android.provider.Settings; 44import android.sax.Element; 45import android.sax.ElementListener; 46import android.sax.RootElement; 47import android.system.ErrnoException; 48import android.system.Os; 49import android.text.TextUtils; 50import android.util.Log; 51import android.util.Xml; 52 53import java.io.BufferedReader; 54import java.io.File; 55import java.io.FileDescriptor; 56import java.io.FileInputStream; 57import java.io.IOException; 58import java.io.InputStreamReader; 59import java.util.ArrayList; 60import java.util.HashMap; 61import java.util.HashSet; 62import java.util.Iterator; 63import 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 */ 110public 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 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 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 = Os.access(path, android.system.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 Cursor c = null; 1189 if (files == null) 1190 files = new String[0]; 1191 1192 for (int i = 0; i < files.length; i++) { 1193 String fullPathString = directory + "/" + files[i]; 1194 existingFiles.add(fullPathString); 1195 } 1196 1197 try { 1198 c = mMediaProvider.query( 1199 mPackageName, 1200 mThumbsUri, 1201 new String [] { "_data" }, 1202 null, 1203 null, 1204 null, null); 1205 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1206 if (c != null && c.moveToFirst()) { 1207 do { 1208 String fullPathString = c.getString(0); 1209 existingFiles.remove(fullPathString); 1210 } while (c.moveToNext()); 1211 } 1212 1213 for (String fileToDelete : existingFiles) { 1214 if (false) 1215 Log.v(TAG, "fileToDelete is " + fileToDelete); 1216 try { 1217 (new File(fileToDelete)).delete(); 1218 } catch (SecurityException ex) { 1219 } 1220 } 1221 1222 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1223 } catch (RemoteException e) { 1224 // We will soon be killed... 1225 } finally { 1226 if (c != null) { 1227 c.close(); 1228 } 1229 } 1230 } 1231 1232 static class MediaBulkDeleter { 1233 StringBuilder whereClause = new StringBuilder(); 1234 ArrayList<String> whereArgs = new ArrayList<String>(100); 1235 final IContentProvider mProvider; 1236 final String mPackageName; 1237 final Uri mBaseUri; 1238 1239 public MediaBulkDeleter(IContentProvider provider, String packageName, Uri baseUri) { 1240 mProvider = provider; 1241 mPackageName = packageName; 1242 mBaseUri = baseUri; 1243 } 1244 1245 public void delete(long id) throws RemoteException { 1246 if (whereClause.length() != 0) { 1247 whereClause.append(","); 1248 } 1249 whereClause.append("?"); 1250 whereArgs.add("" + id); 1251 if (whereArgs.size() > 100) { 1252 flush(); 1253 } 1254 } 1255 public void flush() throws RemoteException { 1256 int size = whereArgs.size(); 1257 if (size > 0) { 1258 String [] foo = new String [size]; 1259 foo = whereArgs.toArray(foo); 1260 int numrows = mProvider.delete(mPackageName, mBaseUri, 1261 MediaStore.MediaColumns._ID + " IN (" + 1262 whereClause.toString() + ")", foo); 1263 //Log.i("@@@@@@@@@", "rows deleted: " + numrows); 1264 whereClause.setLength(0); 1265 whereArgs.clear(); 1266 } 1267 } 1268 } 1269 1270 private void postscan(String[] directories) throws RemoteException { 1271 1272 // handle playlists last, after we know what media files are on the storage. 1273 if (mProcessPlaylists) { 1274 processPlayLists(); 1275 } 1276 1277 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1278 pruneDeadThumbnailFiles(); 1279 1280 // allow GC to clean up 1281 mPlayLists = null; 1282 mMediaProvider = null; 1283 } 1284 1285 private void releaseResources() { 1286 // release the DrmManagerClient resources 1287 if (mDrmManagerClient != null) { 1288 mDrmManagerClient.release(); 1289 mDrmManagerClient = null; 1290 } 1291 } 1292 1293 private void initialize(String volumeName) { 1294 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1295 1296 mAudioUri = Audio.Media.getContentUri(volumeName); 1297 mVideoUri = Video.Media.getContentUri(volumeName); 1298 mImagesUri = Images.Media.getContentUri(volumeName); 1299 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1300 mFilesUri = Files.getContentUri(volumeName); 1301 mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); 1302 1303 if (!volumeName.equals("internal")) { 1304 // we only support playlists on external media 1305 mProcessPlaylists = true; 1306 mProcessGenres = true; 1307 mPlaylistsUri = Playlists.getContentUri(volumeName); 1308 1309 mCaseInsensitivePaths = true; 1310 } 1311 } 1312 1313 public void scanDirectories(String[] directories, String volumeName) { 1314 try { 1315 long start = System.currentTimeMillis(); 1316 initialize(volumeName); 1317 prescan(null, true); 1318 long prescan = System.currentTimeMillis(); 1319 1320 if (ENABLE_BULK_INSERTS) { 1321 // create MediaInserter for bulk inserts 1322 mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500); 1323 } 1324 1325 for (int i = 0; i < directories.length; i++) { 1326 processDirectory(directories[i], mClient); 1327 } 1328 1329 if (ENABLE_BULK_INSERTS) { 1330 // flush remaining inserts 1331 mMediaInserter.flushAll(); 1332 mMediaInserter = null; 1333 } 1334 1335 long scan = System.currentTimeMillis(); 1336 postscan(directories); 1337 long end = System.currentTimeMillis(); 1338 1339 if (false) { 1340 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1341 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1342 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1343 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1344 } 1345 } catch (SQLException e) { 1346 // this might happen if the SD card is removed while the media scanner is running 1347 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1348 } catch (UnsupportedOperationException e) { 1349 // this might happen if the SD card is removed while the media scanner is running 1350 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1351 } catch (RemoteException e) { 1352 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1353 } finally { 1354 releaseResources(); 1355 } 1356 } 1357 1358 // this function is used to scan a single file 1359 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1360 try { 1361 initialize(volumeName); 1362 prescan(path, true); 1363 1364 File file = new File(path); 1365 if (!file.exists()) { 1366 return null; 1367 } 1368 1369 // lastModified is in milliseconds on Files. 1370 long lastModifiedSeconds = file.lastModified() / 1000; 1371 1372 // always scan the file, so we can return the content://media Uri for existing files 1373 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1374 false, true, MediaScanner.isNoMediaPath(path)); 1375 } catch (RemoteException e) { 1376 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1377 return null; 1378 } finally { 1379 releaseResources(); 1380 } 1381 } 1382 1383 private static boolean isNoMediaFile(String path) { 1384 File file = new File(path); 1385 if (file.isDirectory()) return false; 1386 1387 // special case certain file names 1388 // I use regionMatches() instead of substring() below 1389 // to avoid memory allocation 1390 int lastSlash = path.lastIndexOf('/'); 1391 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1392 // ignore those ._* files created by MacOS 1393 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1394 return true; 1395 } 1396 1397 // ignore album art files created by Windows Media Player: 1398 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1399 // and AlbumArt_{...}_Small.jpg 1400 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1401 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1402 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1403 return true; 1404 } 1405 int length = path.length() - lastSlash - 1; 1406 if ((length == 17 && path.regionMatches( 1407 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1408 (length == 10 1409 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1410 return true; 1411 } 1412 } 1413 } 1414 return false; 1415 } 1416 1417 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>(); 1418 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>(); 1419 1420 /* MediaProvider calls this when a .nomedia file is added or removed */ 1421 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) { 1422 synchronized (MediaScanner.class) { 1423 if (clearMediaPaths) { 1424 mMediaPaths.clear(); 1425 } 1426 if (clearNoMediaPaths) { 1427 mNoMediaPaths.clear(); 1428 } 1429 } 1430 } 1431 1432 public static boolean isNoMediaPath(String path) { 1433 if (path == null) { 1434 return false; 1435 } 1436 // return true if file or any parent directory has name starting with a dot 1437 if (path.indexOf("/.") >= 0) { 1438 return true; 1439 } 1440 1441 int firstSlash = path.lastIndexOf('/'); 1442 if (firstSlash <= 0) { 1443 return false; 1444 } 1445 String parent = path.substring(0, firstSlash); 1446 1447 synchronized (MediaScanner.class) { 1448 if (mNoMediaPaths.containsKey(parent)) { 1449 return true; 1450 } else if (!mMediaPaths.containsKey(parent)) { 1451 // check to see if any parent directories have a ".nomedia" file 1452 // start from 1 so we don't bother checking in the root directory 1453 int offset = 1; 1454 while (offset >= 0) { 1455 int slashIndex = path.indexOf('/', offset); 1456 if (slashIndex > offset) { 1457 slashIndex++; // move past slash 1458 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1459 if (file.exists()) { 1460 // we have a .nomedia in one of the parent directories 1461 mNoMediaPaths.put(parent, ""); 1462 return true; 1463 } 1464 } 1465 offset = slashIndex; 1466 } 1467 mMediaPaths.put(parent, ""); 1468 } 1469 } 1470 1471 return isNoMediaFile(path); 1472 } 1473 1474 public void scanMtpFile(String path, String volumeName, int objectHandle, int format) { 1475 initialize(volumeName); 1476 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1477 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1478 File file = new File(path); 1479 long lastModifiedSeconds = file.lastModified() / 1000; 1480 1481 if (!MediaFile.isAudioFileType(fileType) && !MediaFile.isVideoFileType(fileType) && 1482 !MediaFile.isImageFileType(fileType) && !MediaFile.isPlayListFileType(fileType) && 1483 !MediaFile.isDrmFileType(fileType)) { 1484 1485 // no need to use the media scanner, but we need to update last modified and file size 1486 ContentValues values = new ContentValues(); 1487 values.put(Files.FileColumns.SIZE, file.length()); 1488 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1489 try { 1490 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1491 mMediaProvider.update(mPackageName, Files.getMtpObjectsUri(volumeName), values, 1492 "_id=?", whereArgs); 1493 } catch (RemoteException e) { 1494 Log.e(TAG, "RemoteException in scanMtpFile", e); 1495 } 1496 return; 1497 } 1498 1499 mMtpObjectHandle = objectHandle; 1500 Cursor fileList = null; 1501 try { 1502 if (MediaFile.isPlayListFileType(fileType)) { 1503 // build file cache so we can look up tracks in the playlist 1504 prescan(null, true); 1505 1506 FileEntry entry = makeEntryFor(path); 1507 if (entry != null) { 1508 fileList = mMediaProvider.query(mPackageName, mFilesUri, 1509 FILES_PRESCAN_PROJECTION, null, null, null, null); 1510 processPlayList(entry, fileList); 1511 } 1512 } else { 1513 // MTP will create a file entry for us so we don't want to do it in prescan 1514 prescan(path, false); 1515 1516 // always scan the file, so we can return the content://media Uri for existing files 1517 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1518 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1519 } 1520 } catch (RemoteException e) { 1521 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1522 } finally { 1523 mMtpObjectHandle = 0; 1524 if (fileList != null) { 1525 fileList.close(); 1526 } 1527 releaseResources(); 1528 } 1529 } 1530 1531 FileEntry makeEntryFor(String path) { 1532 String where; 1533 String[] selectionArgs; 1534 1535 Cursor c = null; 1536 try { 1537 where = Files.FileColumns.DATA + "=?"; 1538 selectionArgs = new String[] { path }; 1539 c = mMediaProvider.query(mPackageName, mFilesUriNoNotify, FILES_PRESCAN_PROJECTION, 1540 where, selectionArgs, null, null); 1541 if (c.moveToFirst()) { 1542 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1543 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1544 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1545 return new FileEntry(rowId, path, lastModified, format); 1546 } 1547 } catch (RemoteException e) { 1548 } finally { 1549 if (c != null) { 1550 c.close(); 1551 } 1552 } 1553 return null; 1554 } 1555 1556 // returns the number of matching file/directory names, starting from the right 1557 private int matchPaths(String path1, String path2) { 1558 int result = 0; 1559 int end1 = path1.length(); 1560 int end2 = path2.length(); 1561 1562 while (end1 > 0 && end2 > 0) { 1563 int slash1 = path1.lastIndexOf('/', end1 - 1); 1564 int slash2 = path2.lastIndexOf('/', end2 - 1); 1565 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1566 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1567 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1568 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1569 if (start1 < 0) start1 = 0; else start1++; 1570 if (start2 < 0) start2 = 0; else start2++; 1571 int length = end1 - start1; 1572 if (end2 - start2 != length) break; 1573 if (path1.regionMatches(true, start1, path2, start2, length)) { 1574 result++; 1575 end1 = start1 - 1; 1576 end2 = start2 - 1; 1577 } else break; 1578 } 1579 1580 return result; 1581 } 1582 1583 private boolean matchEntries(long rowId, String data) { 1584 1585 int len = mPlaylistEntries.size(); 1586 boolean done = true; 1587 for (int i = 0; i < len; i++) { 1588 PlaylistEntry entry = mPlaylistEntries.get(i); 1589 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1590 continue; // this entry has been matched already 1591 } 1592 done = false; 1593 if (data.equalsIgnoreCase(entry.path)) { 1594 entry.bestmatchid = rowId; 1595 entry.bestmatchlevel = Integer.MAX_VALUE; 1596 continue; // no need for path matching 1597 } 1598 1599 int matchLength = matchPaths(data, entry.path); 1600 if (matchLength > entry.bestmatchlevel) { 1601 entry.bestmatchid = rowId; 1602 entry.bestmatchlevel = matchLength; 1603 } 1604 } 1605 return done; 1606 } 1607 1608 private void cachePlaylistEntry(String line, String playListDirectory) { 1609 PlaylistEntry entry = new PlaylistEntry(); 1610 // watch for trailing whitespace 1611 int entryLength = line.length(); 1612 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1613 // path should be longer than 3 characters. 1614 // avoid index out of bounds errors below by returning here. 1615 if (entryLength < 3) return; 1616 if (entryLength < line.length()) line = line.substring(0, entryLength); 1617 1618 // does entry appear to be an absolute path? 1619 // look for Unix or DOS absolute paths 1620 char ch1 = line.charAt(0); 1621 boolean fullPath = (ch1 == '/' || 1622 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1623 // if we have a relative path, combine entry with playListDirectory 1624 if (!fullPath) 1625 line = playListDirectory + line; 1626 entry.path = line; 1627 //FIXME - should we look for "../" within the path? 1628 1629 mPlaylistEntries.add(entry); 1630 } 1631 1632 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1633 fileList.moveToPosition(-1); 1634 while (fileList.moveToNext()) { 1635 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1636 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1637 if (matchEntries(rowId, data)) { 1638 break; 1639 } 1640 } 1641 1642 int len = mPlaylistEntries.size(); 1643 int index = 0; 1644 for (int i = 0; i < len; i++) { 1645 PlaylistEntry entry = mPlaylistEntries.get(i); 1646 if (entry.bestmatchlevel > 0) { 1647 try { 1648 values.clear(); 1649 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1650 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1651 mMediaProvider.insert(mPackageName, playlistUri, values); 1652 index++; 1653 } catch (RemoteException e) { 1654 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1655 return; 1656 } 1657 } 1658 } 1659 mPlaylistEntries.clear(); 1660 } 1661 1662 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1663 ContentValues values, Cursor fileList) { 1664 BufferedReader reader = null; 1665 try { 1666 File f = new File(path); 1667 if (f.exists()) { 1668 reader = new BufferedReader( 1669 new InputStreamReader(new FileInputStream(f)), 8192); 1670 String line = reader.readLine(); 1671 mPlaylistEntries.clear(); 1672 while (line != null) { 1673 // ignore comment lines, which begin with '#' 1674 if (line.length() > 0 && line.charAt(0) != '#') { 1675 cachePlaylistEntry(line, playListDirectory); 1676 } 1677 line = reader.readLine(); 1678 } 1679 1680 processCachedPlaylist(fileList, values, uri); 1681 } 1682 } catch (IOException e) { 1683 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1684 } finally { 1685 try { 1686 if (reader != null) 1687 reader.close(); 1688 } catch (IOException e) { 1689 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1690 } 1691 } 1692 } 1693 1694 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1695 ContentValues values, Cursor fileList) { 1696 BufferedReader reader = null; 1697 try { 1698 File f = new File(path); 1699 if (f.exists()) { 1700 reader = new BufferedReader( 1701 new InputStreamReader(new FileInputStream(f)), 8192); 1702 String line = reader.readLine(); 1703 mPlaylistEntries.clear(); 1704 while (line != null) { 1705 // ignore comment lines, which begin with '#' 1706 if (line.startsWith("File")) { 1707 int equals = line.indexOf('='); 1708 if (equals > 0) { 1709 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1710 } 1711 } 1712 line = reader.readLine(); 1713 } 1714 1715 processCachedPlaylist(fileList, values, uri); 1716 } 1717 } catch (IOException e) { 1718 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1719 } finally { 1720 try { 1721 if (reader != null) 1722 reader.close(); 1723 } catch (IOException e) { 1724 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1725 } 1726 } 1727 } 1728 1729 class WplHandler implements ElementListener { 1730 1731 final ContentHandler handler; 1732 String playListDirectory; 1733 1734 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1735 this.playListDirectory = playListDirectory; 1736 1737 RootElement root = new RootElement("smil"); 1738 Element body = root.getChild("body"); 1739 Element seq = body.getChild("seq"); 1740 Element media = seq.getChild("media"); 1741 media.setElementListener(this); 1742 1743 this.handler = root.getContentHandler(); 1744 } 1745 1746 @Override 1747 public void start(Attributes attributes) { 1748 String path = attributes.getValue("", "src"); 1749 if (path != null) { 1750 cachePlaylistEntry(path, playListDirectory); 1751 } 1752 } 1753 1754 @Override 1755 public void end() { 1756 } 1757 1758 ContentHandler getContentHandler() { 1759 return handler; 1760 } 1761 } 1762 1763 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1764 ContentValues values, Cursor fileList) { 1765 FileInputStream fis = null; 1766 try { 1767 File f = new File(path); 1768 if (f.exists()) { 1769 fis = new FileInputStream(f); 1770 1771 mPlaylistEntries.clear(); 1772 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1773 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1774 1775 processCachedPlaylist(fileList, values, uri); 1776 } 1777 } catch (SAXException e) { 1778 e.printStackTrace(); 1779 } catch (IOException e) { 1780 e.printStackTrace(); 1781 } finally { 1782 try { 1783 if (fis != null) 1784 fis.close(); 1785 } catch (IOException e) { 1786 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1787 } 1788 } 1789 } 1790 1791 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1792 String path = entry.mPath; 1793 ContentValues values = new ContentValues(); 1794 int lastSlash = path.lastIndexOf('/'); 1795 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1796 Uri uri, membersUri; 1797 long rowId = entry.mRowId; 1798 1799 // make sure we have a name 1800 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1801 if (name == null) { 1802 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1803 if (name == null) { 1804 // extract name from file name 1805 int lastDot = path.lastIndexOf('.'); 1806 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1807 : path.substring(lastSlash + 1, lastDot)); 1808 } 1809 } 1810 1811 values.put(MediaStore.Audio.Playlists.NAME, name); 1812 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1813 1814 if (rowId == 0) { 1815 values.put(MediaStore.Audio.Playlists.DATA, path); 1816 uri = mMediaProvider.insert(mPackageName, mPlaylistsUri, values); 1817 rowId = ContentUris.parseId(uri); 1818 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1819 } else { 1820 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1821 mMediaProvider.update(mPackageName, uri, values, null, null); 1822 1823 // delete members of existing playlist 1824 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1825 mMediaProvider.delete(mPackageName, membersUri, null, null); 1826 } 1827 1828 String playListDirectory = path.substring(0, lastSlash + 1); 1829 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1830 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1831 1832 if (fileType == MediaFile.FILE_TYPE_M3U) { 1833 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1834 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1835 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1836 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1837 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1838 } 1839 } 1840 1841 private void processPlayLists() throws RemoteException { 1842 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1843 Cursor fileList = null; 1844 try { 1845 // use the files uri and projection because we need the format column, 1846 // but restrict the query to just audio files 1847 fileList = mMediaProvider.query(mPackageName, mFilesUri, FILES_PRESCAN_PROJECTION, 1848 "media_type=2", null, null, null); 1849 while (iterator.hasNext()) { 1850 FileEntry entry = iterator.next(); 1851 // only process playlist files if they are new or have been modified since the last scan 1852 if (entry.mLastModifiedChanged) { 1853 processPlayList(entry, fileList); 1854 } 1855 } 1856 } catch (RemoteException e1) { 1857 } finally { 1858 if (fileList != null) { 1859 fileList.close(); 1860 } 1861 } 1862 } 1863 1864 private native void processDirectory(String path, MediaScannerClient client); 1865 private native void processFile(String path, String mimeType, MediaScannerClient client); 1866 public native void setLocale(String locale); 1867 1868 public native byte[] extractAlbumArt(FileDescriptor fd); 1869 1870 private static native final void native_init(); 1871 private native final void native_setup(); 1872 private native final void native_finalize(); 1873 1874 /** 1875 * Releases resources associated with this MediaScanner object. 1876 * It is considered good practice to call this method when 1877 * one is done using the MediaScanner object. After this method 1878 * is called, the MediaScanner object can no longer be used. 1879 */ 1880 public void release() { 1881 native_finalize(); 1882 } 1883 1884 @Override 1885 protected void finalize() { 1886 mContext.getContentResolver().releaseProvider(mMediaProvider); 1887 native_finalize(); 1888 } 1889} 1890