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