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