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