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