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