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