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