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