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