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