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