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