MediaScanner.java revision 1851db6804cbdf8d2c8ede0725e73be6fda0f3c0
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 if ("audio/mp4".equals(mMimeType) && 614 mimeType.startsWith("video")) { 615 // for feature parity with Donut, we force m4a files to keep the 616 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 617 // with a video track 618 return; 619 } 620 mMimeType = mimeType; 621 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 622 } 623 624 /** 625 * Formats the data into a values array suitable for use with the Media 626 * Content Provider. 627 * 628 * @return a map of values 629 */ 630 private ContentValues toValues() { 631 ContentValues map = new ContentValues(); 632 633 map.put(MediaStore.MediaColumns.DATA, mPath); 634 map.put(MediaStore.MediaColumns.TITLE, mTitle); 635 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 636 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 637 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 638 639 if (MediaFile.isVideoFileType(mFileType)) { 640 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 641 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 642 map.put(Video.Media.DURATION, mDuration); 643 // FIXME - add RESOLUTION 644 } else if (MediaFile.isImageFileType(mFileType)) { 645 // FIXME - add DESCRIPTION 646 // map.put(field, value); 647 } else if (MediaFile.isAudioFileType(mFileType)) { 648 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 649 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 650 map.put(Audio.Media.COMPOSER, mComposer); 651 if (mYear != 0) { 652 map.put(Audio.Media.YEAR, mYear); 653 } 654 map.put(Audio.Media.TRACK, mTrack); 655 map.put(Audio.Media.DURATION, mDuration); 656 } 657 return map; 658 } 659 660 private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, 661 boolean alarms, boolean music, boolean podcasts) 662 throws RemoteException { 663 // update database 664 Uri tableUri; 665 boolean isAudio = MediaFile.isAudioFileType(mFileType); 666 boolean isVideo = MediaFile.isVideoFileType(mFileType); 667 boolean isImage = MediaFile.isImageFileType(mFileType); 668 if (isVideo) { 669 tableUri = mVideoUri; 670 } else if (isImage) { 671 tableUri = mImagesUri; 672 } else if (isAudio) { 673 tableUri = mAudioUri; 674 } else { 675 // don't add file to database if not audio, video or image 676 return null; 677 } 678 entry.mTableUri = tableUri; 679 680 // use album artist if artist is missing 681 if (mArtist == null || mArtist.length() == 0) { 682 mArtist = mAlbumArtist; 683 } 684 685 ContentValues values = toValues(); 686 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 687 if (title == null || TextUtils.isEmpty(title.trim())) { 688 title = values.getAsString(MediaStore.MediaColumns.DATA); 689 // extract file name after last slash 690 int lastSlash = title.lastIndexOf('/'); 691 if (lastSlash >= 0) { 692 lastSlash++; 693 if (lastSlash < title.length()) { 694 title = title.substring(lastSlash); 695 } 696 } 697 // truncate the file extension (if any) 698 int lastDot = title.lastIndexOf('.'); 699 if (lastDot > 0) { 700 title = title.substring(0, lastDot); 701 } 702 values.put(MediaStore.MediaColumns.TITLE, title); 703 } 704 String album = values.getAsString(Audio.Media.ALBUM); 705 if (MediaFile.UNKNOWN_STRING.equals(album)) { 706 album = values.getAsString(MediaStore.MediaColumns.DATA); 707 // extract last path segment before file name 708 int lastSlash = album.lastIndexOf('/'); 709 if (lastSlash >= 0) { 710 int previousSlash = 0; 711 while (true) { 712 int idx = album.indexOf('/', previousSlash + 1); 713 if (idx < 0 || idx >= lastSlash) { 714 break; 715 } 716 previousSlash = idx; 717 } 718 if (previousSlash != 0) { 719 album = album.substring(previousSlash + 1, lastSlash); 720 values.put(Audio.Media.ALBUM, album); 721 } 722 } 723 } 724 if (isAudio) { 725 values.put(Audio.Media.IS_RINGTONE, ringtones); 726 values.put(Audio.Media.IS_NOTIFICATION, notifications); 727 values.put(Audio.Media.IS_ALARM, alarms); 728 values.put(Audio.Media.IS_MUSIC, music); 729 values.put(Audio.Media.IS_PODCAST, podcasts); 730 } else if (mFileType == MediaFile.FILE_TYPE_JPEG) { 731 ExifInterface exif = null; 732 try { 733 exif = new ExifInterface(entry.mPath); 734 } catch (IOException ex) { 735 // exif is null 736 } 737 if (exif != null) { 738 float[] latlng = new float[2]; 739 if (exif.getLatLong(latlng)) { 740 values.put(Images.Media.LATITUDE, latlng[0]); 741 values.put(Images.Media.LONGITUDE, latlng[1]); 742 } 743 744 long time = exif.getDateTime(); 745 if (time != -1) { 746 values.put(Images.Media.DATE_TAKEN, time); 747 } 748 749 int orientation = exif.getAttributeInt( 750 ExifInterface.TAG_ORIENTATION, -1); 751 if (orientation != -1) { 752 // We only recognize a subset of orientation tag values. 753 int degree; 754 switch(orientation) { 755 case ExifInterface.ORIENTATION_ROTATE_90: 756 degree = 90; 757 break; 758 case ExifInterface.ORIENTATION_ROTATE_180: 759 degree = 180; 760 break; 761 case ExifInterface.ORIENTATION_ROTATE_270: 762 degree = 270; 763 break; 764 default: 765 degree = 0; 766 break; 767 } 768 values.put(Images.Media.ORIENTATION, degree); 769 } 770 } 771 } 772 773 Uri result = null; 774 long rowId = entry.mRowId; 775 if (rowId == 0) { 776 // new file, insert it 777 result = mMediaProvider.insert(tableUri, values); 778 if (result != null) { 779 rowId = ContentUris.parseId(result); 780 entry.mRowId = rowId; 781 } 782 } else { 783 // updated file 784 result = ContentUris.withAppendedId(tableUri, rowId); 785 mMediaProvider.update(result, values, null, null); 786 } 787 if (mProcessGenres && mGenre != null) { 788 String genre = mGenre; 789 Uri uri = mGenreCache.get(genre); 790 if (uri == null) { 791 Cursor cursor = null; 792 try { 793 // see if the genre already exists 794 cursor = mMediaProvider.query( 795 mGenresUri, 796 GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 797 new String[] { genre }, null); 798 if (cursor == null || cursor.getCount() == 0) { 799 // genre does not exist, so create the genre in the genre table 800 values.clear(); 801 values.put(MediaStore.Audio.Genres.NAME, genre); 802 uri = mMediaProvider.insert(mGenresUri, values); 803 } else { 804 // genre already exists, so compute its Uri 805 cursor.moveToNext(); 806 uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0)); 807 } 808 if (uri != null) { 809 uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY); 810 mGenreCache.put(genre, uri); 811 } 812 } finally { 813 // release the cursor if it exists 814 if (cursor != null) { 815 cursor.close(); 816 } 817 } 818 } 819 820 if (uri != null) { 821 // add entry to audio_genre_map 822 values.clear(); 823 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 824 mMediaProvider.insert(uri, values); 825 } 826 } 827 828 if (notifications && !mDefaultNotificationSet) { 829 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 830 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 831 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 832 mDefaultNotificationSet = true; 833 } 834 } else if (ringtones && !mDefaultRingtoneSet) { 835 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 836 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 837 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 838 mDefaultRingtoneSet = true; 839 } 840 } else if (alarms && !mDefaultAlarmSet) { 841 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 842 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 843 setSettingIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 844 mDefaultAlarmSet = true; 845 } 846 } 847 848 return result; 849 } 850 851 private boolean doesPathHaveFilename(String path, String filename) { 852 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 853 int filenameLength = filename.length(); 854 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 855 pathFilenameStart + filenameLength == path.length(); 856 } 857 858 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 859 860 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 861 settingName); 862 863 if (TextUtils.isEmpty(existingSettingValue)) { 864 // Set the setting to the given URI 865 Settings.System.putString(mContext.getContentResolver(), settingName, 866 ContentUris.withAppendedId(uri, rowId).toString()); 867 } 868 } 869 870 public void addNoMediaFolder(String path) { 871 ContentValues values = new ContentValues(); 872 values.put(MediaStore.Images.ImageColumns.DATA, ""); 873 String [] pathSpec = new String[] {path + '%'}; 874 try { 875 mMediaProvider.update(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values, 876 MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec); 877 mMediaProvider.update(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values, 878 MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec); 879 mMediaProvider.update(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values, 880 MediaStore.Images.ImageColumns.DATA + " LIKE ?", pathSpec); 881 } catch (RemoteException e) { 882 throw new RuntimeException(); 883 } 884 } 885 886 }; // end of anonymous MediaScannerClient instance 887 888 private void prescan(String filePath) throws RemoteException { 889 Cursor c = null; 890 String where = null; 891 String[] selectionArgs = null; 892 893 if (mFileCache == null) { 894 mFileCache = new HashMap<String, FileCacheEntry>(); 895 } else { 896 mFileCache.clear(); 897 } 898 if (mPlayLists == null) { 899 mPlayLists = new ArrayList<FileCacheEntry>(); 900 } else { 901 mPlayLists.clear(); 902 } 903 904 // Build the list of files from the content provider 905 try { 906 // Read existing files from the audio table 907 if (filePath != null) { 908 where = MediaStore.Audio.Media.DATA + "=?"; 909 selectionArgs = new String[] { filePath }; 910 } 911 c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); 912 913 if (c != null) { 914 try { 915 while (c.moveToNext()) { 916 long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); 917 String path = c.getString(PATH_AUDIO_COLUMN_INDEX); 918 long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); 919 920 String key = path; 921 if (mCaseInsensitivePaths) { 922 key = path.toLowerCase(); 923 } 924 mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, 925 lastModified)); 926 } 927 } finally { 928 c.close(); 929 c = null; 930 } 931 } 932 933 // Read existing files from the video table 934 if (filePath != null) { 935 where = MediaStore.Video.Media.DATA + "=?"; 936 } else { 937 where = null; 938 } 939 c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); 940 941 if (c != null) { 942 try { 943 while (c.moveToNext()) { 944 long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); 945 String path = c.getString(PATH_VIDEO_COLUMN_INDEX); 946 long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); 947 948 String key = path; 949 if (mCaseInsensitivePaths) { 950 key = path.toLowerCase(); 951 } 952 mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, 953 lastModified)); 954 } 955 } finally { 956 c.close(); 957 c = null; 958 } 959 } 960 961 // Read existing files from the images table 962 if (filePath != null) { 963 where = MediaStore.Images.Media.DATA + "=?"; 964 } else { 965 where = null; 966 } 967 mOriginalCount = 0; 968 c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); 969 970 if (c != null) { 971 try { 972 mOriginalCount = c.getCount(); 973 while (c.moveToNext()) { 974 long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); 975 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 976 long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); 977 978 String key = path; 979 if (mCaseInsensitivePaths) { 980 key = path.toLowerCase(); 981 } 982 mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, 983 lastModified)); 984 } 985 } finally { 986 c.close(); 987 c = null; 988 } 989 } 990 991 if (mProcessPlaylists) { 992 // Read existing files from the playlists table 993 if (filePath != null) { 994 where = MediaStore.Audio.Playlists.DATA + "=?"; 995 } else { 996 where = null; 997 } 998 c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); 999 1000 if (c != null) { 1001 try { 1002 while (c.moveToNext()) { 1003 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 1004 1005 if (path != null && path.length() > 0) { 1006 long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); 1007 long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); 1008 1009 String key = path; 1010 if (mCaseInsensitivePaths) { 1011 key = path.toLowerCase(); 1012 } 1013 mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, 1014 lastModified)); 1015 } 1016 } 1017 } finally { 1018 c.close(); 1019 c = null; 1020 } 1021 } 1022 } 1023 } 1024 finally { 1025 if (c != null) { 1026 c.close(); 1027 } 1028 } 1029 } 1030 1031 private boolean inScanDirectory(String path, String[] directories) { 1032 for (int i = 0; i < directories.length; i++) { 1033 if (path.startsWith(directories[i])) { 1034 return true; 1035 } 1036 } 1037 return false; 1038 } 1039 1040 private void pruneDeadThumbnailFiles() { 1041 HashSet<String> existingFiles = new HashSet<String>(); 1042 String directory = "/sdcard/DCIM/.thumbnails"; 1043 String [] files = (new File(directory)).list(); 1044 if (files == null) 1045 files = new String[0]; 1046 1047 for (int i = 0; i < files.length; i++) { 1048 String fullPathString = directory + "/" + files[i]; 1049 existingFiles.add(fullPathString); 1050 } 1051 1052 try { 1053 Cursor c = mMediaProvider.query( 1054 mThumbsUri, 1055 new String [] { "_data" }, 1056 null, 1057 null, 1058 null); 1059 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 1060 if (c != null && c.moveToFirst()) { 1061 do { 1062 String fullPathString = c.getString(0); 1063 existingFiles.remove(fullPathString); 1064 } while (c.moveToNext()); 1065 } 1066 1067 for (String fileToDelete : existingFiles) { 1068 if (Config.LOGV) 1069 Log.v(TAG, "fileToDelete is " + fileToDelete); 1070 try { 1071 (new File(fileToDelete)).delete(); 1072 } catch (SecurityException ex) { 1073 } 1074 } 1075 1076 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 1077 if (c != null) { 1078 c.close(); 1079 } 1080 } catch (RemoteException e) { 1081 // We will soon be killed... 1082 } 1083 } 1084 1085 private void postscan(String[] directories) throws RemoteException { 1086 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1087 1088 while (iterator.hasNext()) { 1089 FileCacheEntry entry = iterator.next(); 1090 String path = entry.mPath; 1091 1092 // remove database entries for files that no longer exist. 1093 boolean fileMissing = false; 1094 1095 if (!entry.mSeenInFileSystem) { 1096 if (inScanDirectory(path, directories)) { 1097 // we didn't see this file in the scan directory. 1098 fileMissing = true; 1099 } else { 1100 // the file is outside of our scan directory, 1101 // so we need to check for file existence here. 1102 File testFile = new File(path); 1103 if (!testFile.exists()) { 1104 fileMissing = true; 1105 } 1106 } 1107 } 1108 1109 if (fileMissing) { 1110 // do not delete missing playlists, since they may have been modified by the user. 1111 // the user can delete them in the media player instead. 1112 // instead, clear the path and lastModified fields in the row 1113 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1114 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1115 1116 if (MediaFile.isPlayListFileType(fileType)) { 1117 ContentValues values = new ContentValues(); 1118 values.put(MediaStore.Audio.Playlists.DATA, ""); 1119 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 1120 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); 1121 } else { 1122 mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); 1123 iterator.remove(); 1124 } 1125 } 1126 } 1127 1128 // handle playlists last, after we know what media files are on the storage. 1129 if (mProcessPlaylists) { 1130 processPlayLists(); 1131 } 1132 1133 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 1134 pruneDeadThumbnailFiles(); 1135 1136 // allow GC to clean up 1137 mGenreCache = null; 1138 mPlayLists = null; 1139 mFileCache = null; 1140 mMediaProvider = null; 1141 } 1142 1143 private void initialize(String volumeName) { 1144 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 1145 1146 mAudioUri = Audio.Media.getContentUri(volumeName); 1147 mVideoUri = Video.Media.getContentUri(volumeName); 1148 mImagesUri = Images.Media.getContentUri(volumeName); 1149 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1150 1151 if (!volumeName.equals("internal")) { 1152 // we only support playlists on external media 1153 mProcessPlaylists = true; 1154 mProcessGenres = true; 1155 mGenreCache = new HashMap<String, Uri>(); 1156 mGenresUri = Genres.getContentUri(volumeName); 1157 mPlaylistsUri = Playlists.getContentUri(volumeName); 1158 // assuming external storage is FAT (case insensitive), except on the simulator. 1159 if ( Process.supportsProcesses()) { 1160 mCaseInsensitivePaths = true; 1161 } 1162 } 1163 } 1164 1165 public void scanDirectories(String[] directories, String volumeName) { 1166 try { 1167 long start = System.currentTimeMillis(); 1168 initialize(volumeName); 1169 prescan(null); 1170 long prescan = System.currentTimeMillis(); 1171 1172 for (int i = 0; i < directories.length; i++) { 1173 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1174 } 1175 long scan = System.currentTimeMillis(); 1176 postscan(directories); 1177 long end = System.currentTimeMillis(); 1178 1179 if (Config.LOGD) { 1180 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1181 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1182 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1183 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1184 } 1185 } catch (SQLException e) { 1186 // this might happen if the SD card is removed while the media scanner is running 1187 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1188 } catch (UnsupportedOperationException e) { 1189 // this might happen if the SD card is removed while the media scanner is running 1190 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1191 } catch (RemoteException e) { 1192 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1193 } 1194 } 1195 1196 // this function is used to scan a single file 1197 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1198 try { 1199 initialize(volumeName); 1200 prescan(path); 1201 1202 File file = new File(path); 1203 // always scan the file, so we can return the content://media Uri for existing files 1204 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1205 } catch (RemoteException e) { 1206 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1207 return null; 1208 } 1209 } 1210 1211 // returns the number of matching file/directory names, starting from the right 1212 private int matchPaths(String path1, String path2) { 1213 int result = 0; 1214 int end1 = path1.length(); 1215 int end2 = path2.length(); 1216 1217 while (end1 > 0 && end2 > 0) { 1218 int slash1 = path1.lastIndexOf('/', end1 - 1); 1219 int slash2 = path2.lastIndexOf('/', end2 - 1); 1220 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1221 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1222 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1223 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1224 if (start1 < 0) start1 = 0; else start1++; 1225 if (start2 < 0) start2 = 0; else start2++; 1226 int length = end1 - start1; 1227 if (end2 - start2 != length) break; 1228 if (path1.regionMatches(true, start1, path2, start2, length)) { 1229 result++; 1230 end1 = start1 - 1; 1231 end2 = start2 - 1; 1232 } else break; 1233 } 1234 1235 return result; 1236 } 1237 1238 private boolean addPlayListEntry(String entry, String playListDirectory, 1239 Uri uri, ContentValues values, int index) { 1240 1241 // watch for trailing whitespace 1242 int entryLength = entry.length(); 1243 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1244 // path should be longer than 3 characters. 1245 // avoid index out of bounds errors below by returning here. 1246 if (entryLength < 3) return false; 1247 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1248 1249 // does entry appear to be an absolute path? 1250 // look for Unix or DOS absolute paths 1251 char ch1 = entry.charAt(0); 1252 boolean fullPath = (ch1 == '/' || 1253 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1254 // if we have a relative path, combine entry with playListDirectory 1255 if (!fullPath) 1256 entry = playListDirectory + entry; 1257 1258 //FIXME - should we look for "../" within the path? 1259 1260 // best matching MediaFile for the play list entry 1261 FileCacheEntry bestMatch = null; 1262 1263 // number of rightmost file/directory names for bestMatch 1264 int bestMatchLength = 0; 1265 1266 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1267 while (iterator.hasNext()) { 1268 FileCacheEntry cacheEntry = iterator.next(); 1269 String path = cacheEntry.mPath; 1270 1271 if (path.equalsIgnoreCase(entry)) { 1272 bestMatch = cacheEntry; 1273 break; // don't bother continuing search 1274 } 1275 1276 int matchLength = matchPaths(path, entry); 1277 if (matchLength > bestMatchLength) { 1278 bestMatch = cacheEntry; 1279 bestMatchLength = matchLength; 1280 } 1281 } 1282 1283 // if the match is not for an audio file, bail out 1284 if (bestMatch == null || ! mAudioUri.equals(bestMatch.mTableUri)) { 1285 return false; 1286 } 1287 1288 try { 1289 // OK, now we need to add this to the database 1290 values.clear(); 1291 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1292 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1293 mMediaProvider.insert(uri, values); 1294 } catch (RemoteException e) { 1295 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1296 return false; 1297 } 1298 1299 return true; 1300 } 1301 1302 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1303 BufferedReader reader = null; 1304 try { 1305 File f = new File(path); 1306 if (f.exists()) { 1307 reader = new BufferedReader( 1308 new InputStreamReader(new FileInputStream(f)), 8192); 1309 String line = reader.readLine(); 1310 int index = 0; 1311 while (line != null) { 1312 // ignore comment lines, which begin with '#' 1313 if (line.length() > 0 && line.charAt(0) != '#') { 1314 values.clear(); 1315 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1316 index++; 1317 } 1318 line = reader.readLine(); 1319 } 1320 } 1321 } catch (IOException e) { 1322 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1323 } finally { 1324 try { 1325 if (reader != null) 1326 reader.close(); 1327 } catch (IOException e) { 1328 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1329 } 1330 } 1331 } 1332 1333 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1334 BufferedReader reader = null; 1335 try { 1336 File f = new File(path); 1337 if (f.exists()) { 1338 reader = new BufferedReader( 1339 new InputStreamReader(new FileInputStream(f)), 8192); 1340 String line = reader.readLine(); 1341 int index = 0; 1342 while (line != null) { 1343 // ignore comment lines, which begin with '#' 1344 if (line.startsWith("File")) { 1345 int equals = line.indexOf('='); 1346 if (equals > 0) { 1347 values.clear(); 1348 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1349 index++; 1350 } 1351 } 1352 line = reader.readLine(); 1353 } 1354 } 1355 } catch (IOException e) { 1356 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1357 } finally { 1358 try { 1359 if (reader != null) 1360 reader.close(); 1361 } catch (IOException e) { 1362 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1363 } 1364 } 1365 } 1366 1367 class WplHandler implements ElementListener { 1368 1369 final ContentHandler handler; 1370 String playListDirectory; 1371 Uri uri; 1372 ContentValues values = new ContentValues(); 1373 int index = 0; 1374 1375 public WplHandler(String playListDirectory, Uri uri) { 1376 this.playListDirectory = playListDirectory; 1377 this.uri = uri; 1378 1379 RootElement root = new RootElement("smil"); 1380 Element body = root.getChild("body"); 1381 Element seq = body.getChild("seq"); 1382 Element media = seq.getChild("media"); 1383 media.setElementListener(this); 1384 1385 this.handler = root.getContentHandler(); 1386 } 1387 1388 public void start(Attributes attributes) { 1389 String path = attributes.getValue("", "src"); 1390 if (path != null) { 1391 values.clear(); 1392 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1393 index++; 1394 } 1395 } 1396 } 1397 1398 public void end() { 1399 } 1400 1401 ContentHandler getContentHandler() { 1402 return handler; 1403 } 1404 } 1405 1406 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1407 FileInputStream fis = null; 1408 try { 1409 File f = new File(path); 1410 if (f.exists()) { 1411 fis = new FileInputStream(f); 1412 1413 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1414 } 1415 } catch (SAXException e) { 1416 e.printStackTrace(); 1417 } catch (IOException e) { 1418 e.printStackTrace(); 1419 } finally { 1420 try { 1421 if (fis != null) 1422 fis.close(); 1423 } catch (IOException e) { 1424 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1425 } 1426 } 1427 } 1428 1429 private void processPlayLists() throws RemoteException { 1430 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1431 while (iterator.hasNext()) { 1432 FileCacheEntry entry = iterator.next(); 1433 String path = entry.mPath; 1434 1435 // only process playlist files if they are new or have been modified since the last scan 1436 if (entry.mLastModifiedChanged) { 1437 ContentValues values = new ContentValues(); 1438 int lastSlash = path.lastIndexOf('/'); 1439 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1440 Uri uri, membersUri; 1441 long rowId = entry.mRowId; 1442 if (rowId == 0) { 1443 // Create a new playlist 1444 1445 int lastDot = path.lastIndexOf('.'); 1446 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1447 values.put(MediaStore.Audio.Playlists.NAME, name); 1448 values.put(MediaStore.Audio.Playlists.DATA, path); 1449 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1450 uri = mMediaProvider.insert(mPlaylistsUri, values); 1451 rowId = ContentUris.parseId(uri); 1452 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1453 } else { 1454 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1455 1456 // update lastModified value of existing playlist 1457 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1458 mMediaProvider.update(uri, values, null, null); 1459 1460 // delete members of existing playlist 1461 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1462 mMediaProvider.delete(membersUri, null, null); 1463 } 1464 1465 String playListDirectory = path.substring(0, lastSlash + 1); 1466 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1467 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1468 1469 if (fileType == MediaFile.FILE_TYPE_M3U) 1470 processM3uPlayList(path, playListDirectory, membersUri, values); 1471 else if (fileType == MediaFile.FILE_TYPE_PLS) 1472 processPlsPlayList(path, playListDirectory, membersUri, values); 1473 else if (fileType == MediaFile.FILE_TYPE_WPL) 1474 processWplPlayList(path, playListDirectory, membersUri); 1475 1476 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1477 null, null); 1478 try { 1479 if (cursor == null || cursor.getCount() == 0) { 1480 Log.d(TAG, "playlist is empty - deleting"); 1481 mMediaProvider.delete(uri, null, null); 1482 } 1483 } finally { 1484 if (cursor != null) cursor.close(); 1485 } 1486 } 1487 } 1488 } 1489 1490 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1491 private native void processFile(String path, String mimeType, MediaScannerClient client); 1492 public native void setLocale(String locale); 1493 1494 public native byte[] extractAlbumArt(FileDescriptor fd); 1495 1496 private static native final void native_init(); 1497 private native final void native_setup(); 1498 private native final void native_finalize(); 1499 @Override 1500 protected void finalize() { 1501 mContext.getContentResolver().releaseProvider(mMediaProvider); 1502 native_finalize(); 1503 } 1504} 1505