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