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