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