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