MediaScanner.java revision 18a38b9f41fa818cccc50a7fd372c694a9c8149a
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() 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 Cursor fileList = null; 1400 try { 1401 if (MediaFile.isPlayListFileType(fileType)) { 1402 // build file cache so we can look up tracks in the playlist 1403 prescan(null, true); 1404 1405 FileEntry entry = makeEntryFor(path); 1406 if (entry != null) { 1407 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1408 null, null, null, null); 1409 processPlayList(entry, fileList); 1410 } 1411 } else { 1412 // MTP will create a file entry for us so we don't want to do it in prescan 1413 prescan(path, false); 1414 1415 // always scan the file, so we can return the content://media Uri for existing files 1416 mClient.doScanFile(path, mediaFileType.mimeType, lastModifiedSeconds, file.length(), 1417 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1418 } 1419 } catch (RemoteException e) { 1420 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1421 } finally { 1422 mMtpObjectHandle = 0; 1423 if (fileList != null) { 1424 fileList.close(); 1425 } 1426 } 1427 } 1428 1429 FileEntry makeEntryFor(String path) { 1430 String key = path; 1431 String where; 1432 String[] selectionArgs; 1433 if (mCaseInsensitivePaths) { 1434 where = Files.FileColumns.DATA + " LIKE ?"; 1435 selectionArgs = new String[] { path }; 1436 } else { 1437 where = Files.FileColumns.DATA + "=?"; 1438 selectionArgs = new String[] { path }; 1439 } 1440 1441 Cursor c = null; 1442 try { 1443 c = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1444 where, selectionArgs, null, null); 1445 if (c.moveToNext()) { 1446 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1447 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1448 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1449 return new FileEntry(rowId, path, lastModified, format); 1450 } 1451 } catch (RemoteException e) { 1452 } finally { 1453 if (c != null) { 1454 c.close(); 1455 } 1456 } 1457 return null; 1458 } 1459 1460 // returns the number of matching file/directory names, starting from the right 1461 private int matchPaths(String path1, String path2) { 1462 int result = 0; 1463 int end1 = path1.length(); 1464 int end2 = path2.length(); 1465 1466 while (end1 > 0 && end2 > 0) { 1467 int slash1 = path1.lastIndexOf('/', end1 - 1); 1468 int slash2 = path2.lastIndexOf('/', end2 - 1); 1469 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1470 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1471 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1472 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1473 if (start1 < 0) start1 = 0; else start1++; 1474 if (start2 < 0) start2 = 0; else start2++; 1475 int length = end1 - start1; 1476 if (end2 - start2 != length) break; 1477 if (path1.regionMatches(true, start1, path2, start2, length)) { 1478 result++; 1479 end1 = start1 - 1; 1480 end2 = start2 - 1; 1481 } else break; 1482 } 1483 1484 return result; 1485 } 1486 1487 private boolean addPlayListEntry(String entry, String playListDirectory, 1488 Uri uri, ContentValues values, int index, Cursor fileList) { 1489 1490 // watch for trailing whitespace 1491 int entryLength = entry.length(); 1492 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1493 // path should be longer than 3 characters. 1494 // avoid index out of bounds errors below by returning here. 1495 if (entryLength < 3) return false; 1496 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1497 1498 // does entry appear to be an absolute path? 1499 // look for Unix or DOS absolute paths 1500 char ch1 = entry.charAt(0); 1501 boolean fullPath = (ch1 == '/' || 1502 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1503 // if we have a relative path, combine entry with playListDirectory 1504 if (!fullPath) 1505 entry = playListDirectory + entry; 1506 1507 //FIXME - should we look for "../" within the path? 1508 1509 // best matching MediaFile for the play list entry 1510 FileEntry bestMatch = null; 1511 1512 // number of rightmost file/directory names for bestMatch 1513 int bestMatchLength = 0; 1514 1515 if (fileList != null) { 1516 int count = fileList.getCount(); 1517 // Backing up a little in the cursor helps when the files in the 1518 // playlist are not in the same order as they are in the database 1519 // but are still close. 1520 fileList.move(-1000); 1521 while(--count >= 0) { 1522 if (!fileList.moveToNext()) { 1523 fileList.moveToFirst(); 1524 } 1525 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1526 String path = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1527 int format = fileList.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1528 long lastModified = fileList.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1529 1530 if (path.equalsIgnoreCase(entry)) { 1531 bestMatch = new FileEntry(rowId, path, lastModified, format); 1532 break; // don't bother continuing search 1533 } 1534 1535 int matchLength = matchPaths(path, entry); 1536 if (matchLength > bestMatchLength) { 1537 bestMatch = new FileEntry(rowId, path, lastModified, format); 1538 bestMatchLength = matchLength; 1539 } 1540 } 1541 } 1542 1543 if (bestMatch == null) { 1544 return false; 1545 } 1546 1547 try { 1548 // check rowid is set. Rowid may be missing if it is inserted by bulkInsert(). 1549 if (bestMatch.mRowId == 0) { 1550 Cursor c = mMediaProvider.query(mAudioUri, ID_PROJECTION, 1551 MediaStore.Files.FileColumns.DATA + "=?", 1552 new String[] { bestMatch.mPath }, null, null); 1553 if (c != null) { 1554 if (c.moveToNext()) { 1555 bestMatch.mRowId = c.getLong(0); 1556 } 1557 c.close(); 1558 } 1559 if (bestMatch.mRowId == 0) { 1560 return false; 1561 } 1562 } 1563 // OK, now we are ready to add this to the database 1564 values.clear(); 1565 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1566 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1567 mMediaProvider.insert(uri, values); 1568 } catch (RemoteException e) { 1569 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1570 return false; 1571 } 1572 1573 return true; 1574 } 1575 1576 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1577 ContentValues values, Cursor fileList) { 1578 BufferedReader reader = null; 1579 try { 1580 File f = new File(path); 1581 if (f.exists()) { 1582 reader = new BufferedReader( 1583 new InputStreamReader(new FileInputStream(f)), 8192); 1584 String line = reader.readLine(); 1585 int index = 0; 1586 while (line != null) { 1587 // ignore comment lines, which begin with '#' 1588 if (line.length() > 0 && line.charAt(0) != '#') { 1589 values.clear(); 1590 if (addPlayListEntry(line, playListDirectory, uri, values, index, fileList)) 1591 index++; 1592 } 1593 line = reader.readLine(); 1594 } 1595 } 1596 } catch (IOException e) { 1597 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1598 } finally { 1599 try { 1600 if (reader != null) 1601 reader.close(); 1602 } catch (IOException e) { 1603 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1604 } 1605 } 1606 } 1607 1608 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1609 ContentValues values, Cursor fileList) { 1610 BufferedReader reader = null; 1611 try { 1612 File f = new File(path); 1613 if (f.exists()) { 1614 reader = new BufferedReader( 1615 new InputStreamReader(new FileInputStream(f)), 8192); 1616 String line = reader.readLine(); 1617 int index = 0; 1618 while (line != null) { 1619 // ignore comment lines, which begin with '#' 1620 if (line.startsWith("File")) { 1621 int equals = line.indexOf('='); 1622 if (equals > 0) { 1623 values.clear(); 1624 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, 1625 uri, values, index, fileList)) 1626 index++; 1627 } 1628 } 1629 line = reader.readLine(); 1630 } 1631 } 1632 } catch (IOException e) { 1633 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1634 } finally { 1635 try { 1636 if (reader != null) 1637 reader.close(); 1638 } catch (IOException e) { 1639 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1640 } 1641 } 1642 } 1643 1644 class WplHandler implements ElementListener { 1645 1646 final ContentHandler handler; 1647 String playListDirectory; 1648 Uri uri; 1649 Cursor fileList; 1650 ContentValues values = new ContentValues(); 1651 int index = 0; 1652 1653 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1654 this.playListDirectory = playListDirectory; 1655 this.uri = uri; 1656 this.fileList = fileList; 1657 1658 RootElement root = new RootElement("smil"); 1659 Element body = root.getChild("body"); 1660 Element seq = body.getChild("seq"); 1661 Element media = seq.getChild("media"); 1662 media.setElementListener(this); 1663 1664 this.handler = root.getContentHandler(); 1665 } 1666 1667 @Override 1668 public void start(Attributes attributes) { 1669 String path = attributes.getValue("", "src"); 1670 if (path != null) { 1671 values.clear(); 1672 if (addPlayListEntry(path, playListDirectory, uri, values, index, fileList)) { 1673 index++; 1674 } 1675 } 1676 } 1677 1678 public void end() { 1679 } 1680 1681 ContentHandler getContentHandler() { 1682 return handler; 1683 } 1684 } 1685 1686 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1687 Cursor fileList) { 1688 FileInputStream fis = null; 1689 try { 1690 File f = new File(path); 1691 if (f.exists()) { 1692 fis = new FileInputStream(f); 1693 1694 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1695 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1696 } 1697 } catch (SAXException e) { 1698 e.printStackTrace(); 1699 } catch (IOException e) { 1700 e.printStackTrace(); 1701 } finally { 1702 try { 1703 if (fis != null) 1704 fis.close(); 1705 } catch (IOException e) { 1706 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1707 } 1708 } 1709 } 1710 1711 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1712 String path = entry.mPath; 1713 ContentValues values = new ContentValues(); 1714 int lastSlash = path.lastIndexOf('/'); 1715 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1716 Uri uri, membersUri; 1717 long rowId = entry.mRowId; 1718 1719 // make sure we have a name 1720 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1721 if (name == null) { 1722 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1723 if (name == null) { 1724 // extract name from file name 1725 int lastDot = path.lastIndexOf('.'); 1726 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1727 : path.substring(lastSlash + 1, lastDot)); 1728 } 1729 } 1730 1731 values.put(MediaStore.Audio.Playlists.NAME, name); 1732 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1733 1734 if (rowId == 0) { 1735 values.put(MediaStore.Audio.Playlists.DATA, path); 1736 uri = mMediaProvider.insert(mPlaylistsUri, values); 1737 rowId = ContentUris.parseId(uri); 1738 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1739 } else { 1740 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1741 mMediaProvider.update(uri, values, null, null); 1742 1743 // delete members of existing playlist 1744 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1745 mMediaProvider.delete(membersUri, null, null); 1746 } 1747 1748 String playListDirectory = path.substring(0, lastSlash + 1); 1749 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1750 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1751 1752 if (fileType == MediaFile.FILE_TYPE_M3U) { 1753 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1754 } else if (fileType == MediaFile.FILE_TYPE_PLS) { 1755 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1756 } else if (fileType == MediaFile.FILE_TYPE_WPL) { 1757 processWplPlayList(path, playListDirectory, membersUri, fileList); 1758 } 1759 } 1760 1761 private void processPlayLists() throws RemoteException { 1762 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1763 Cursor fileList = null; 1764 try { 1765 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1766 null, null, null, null); 1767 while (iterator.hasNext()) { 1768 FileEntry entry = iterator.next(); 1769 // only process playlist files if they are new or have been modified since the last scan 1770 if (entry.mLastModifiedChanged) { 1771 processPlayList(entry, fileList); 1772 } 1773 } 1774 } catch (RemoteException e1) { 1775 } finally { 1776 if (fileList != null) { 1777 fileList.close(); 1778 } 1779 } 1780 } 1781 1782 private native void processDirectory(String path, MediaScannerClient client); 1783 private native void processFile(String path, String mimeType, MediaScannerClient client); 1784 public native void setLocale(String locale); 1785 1786 public native byte[] extractAlbumArt(FileDescriptor fd); 1787 1788 private static native final void native_init(); 1789 private native final void native_setup(); 1790 private native final void native_finalize(); 1791 1792 /** 1793 * Releases resouces associated with this MediaScanner object. 1794 * It is considered good practice to call this method when 1795 * one is done using the MediaScanner object. After this method 1796 * is called, the MediaScanner object can no longer be used. 1797 */ 1798 public void release() { 1799 native_finalize(); 1800 } 1801 1802 @Override 1803 protected void finalize() { 1804 mContext.getContentResolver().releaseProvider(mMediaProvider); 1805 native_finalize(); 1806 } 1807} 1808