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