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