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