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